Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c269d95398 | ||
|
|
45c7b24420 | ||
|
|
082d5dc952 | ||
|
|
55af1120bf | ||
|
|
35864b4de7 | ||
|
|
621cb1913f | ||
|
|
3e3e3f1c0b | ||
|
|
cdf0374020 | ||
|
|
ec406889f7 | ||
|
|
b731a3bc36 | ||
|
|
27e15820f7 |
No files matched your search
@@ -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`)
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NIX_PATH: nixpkgs=channel:nixos-25.05
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_VERSION: "1.86"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
Generated
+7
-7
@@ -657,12 +657,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.1"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix 0.30.1",
|
||||
"nix 0.31.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -1485,9 +1485,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -1709,9 +1709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
|
||||
+52
-12
@@ -1,8 +1,8 @@
|
||||
# REST API `/api/v2`
|
||||
|
||||
The Qitech Control Panel exposes a small HTTP interface for discovering machines and reading their current values. All examples below assume you are connected to the panel’s Ethernet subnet and talk directly to the panel at:
|
||||
The panel behaves like any other NetworkManager-managed linux and is able to either receive an IP-Address over DHCP or a statically defined one. Where the static IP is most likely the easiest to work with.
|
||||
|
||||
- **Base URL:** `http://10.10.10.1:3001`
|
||||
- **Base URL:** `https://IP_ADDRESS`
|
||||
|
||||
> **Schema note:** This documentation intentionally stays light on field details beyond what is shown in the examples. For the exact datatypes and complete payload shapes, refer to the corresponding Rust types (linked below).
|
||||
|
||||
@@ -10,17 +10,57 @@ The Qitech Control Panel exposes a small HTTP interface for discovering machines
|
||||
|
||||
## Authentication
|
||||
|
||||
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks. It is therefore suggested to run the entire production line in isolated operation.
|
||||
The panel performs Basic-Auth authentication.
|
||||
For EVERY request made over HTTPS, Basic Auth is required.
|
||||
|
||||
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity. To let the outside world communicate with the production line, a router should be placed at the network bondary bridging the production line's subnet and the rest of the network. The router can run in **client/bridge mode** on the line's subnet and expose the panel's API via **port forwading** or through a **reverse proxy** where one can add authentication, logging, rate limiting, etc.
|
||||
To set the password on the Panel for Basic Auth, do the following:
|
||||
1. Open a Terminal
|
||||
2. cd /home/qitech/control
|
||||
3. ./credentials.sh
|
||||
|
||||
The following graphic depicts the suggested network topology for a single production line. Many lines could potentially share the same router.
|
||||
The credentials.sh script sets and prints the username and password for the api ONCE in the console.
|
||||
Afterwards its accessible in machine_password in /tmp/ until the machine shuts down.
|
||||
If you ever forget your password you can simply run ./credentials.sh again, resetting the password used, however the username will always be machine
|
||||
|
||||
<p align="center">
|
||||
<img width="769" height="528" alt="Suggested network topology" src="./assets/subnet.png" />
|
||||
</p>
|
||||
Example request with curl:
|
||||
```
|
||||
# username=machine
|
||||
# password=VoMZKZK4K4ZIvON+OU2ZcCXy
|
||||
# With -k you can ignore curl's self signed certificate error
|
||||
curl https://192.168.130.195/api/v2/machine -k -u machine:VoMZKZK4K4ZIvON+OU2ZcCXy
|
||||
```
|
||||
|
||||
|
||||
## Handling Self signed Certificate Warnings
|
||||
|
||||
For HTTPS we use self-signed certificates.
|
||||
These are untrusted by default in browsers,tools like curl and https libraries.
|
||||
|
||||
For most programs you can either ignore this warning or you can manually trust the certificate on your system.
|
||||
How these are installed depends on your system.
|
||||
|
||||
On the panel pc, the root Certificate is located at: /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
|
||||
|
||||
You need to copy this file to an usb drive or copy it some other way and add it as a trusted root certificate in your OS.
|
||||
|
||||
Please refer to your OS' documentation or search the Internet about how to install a trusted SSL certificate.
|
||||
|
||||
|
||||
## Bypassing HTTPS and Authentication
|
||||
If you have an isolated network and dont wish to perform Authentication, then you can achieve this by:
|
||||
1. Forking our repository
|
||||
2. Changing the Open Ports in nixos/os/configuration.nix:
|
||||
```
|
||||
# All the way at the Bottom
|
||||
networking.firewall.allowedUDPPorts = [ 53 67 69 ];
|
||||
# Change this
|
||||
networking.firewall.allowedTCPPorts = [ 443 ];
|
||||
# To this
|
||||
networking.firewall.allowedTCPPorts = [ 3001 ];
|
||||
```
|
||||
|
||||
Again, you should only ever do this if you have your own reverse proxy with authentication or you have an isolated network.
|
||||
|
||||
If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`.
|
||||
|
||||
---
|
||||
|
||||
@@ -38,7 +78,7 @@ Each machine also includes a `legacy_id` to support older **v1** workflows. If a
|
||||
### Example request
|
||||
|
||||
```bash
|
||||
curl -X GET "http://10.10.10.1:3001/api/v2/machine"
|
||||
curl -X GET "https://IP_ADDRESS/api/v2/machine" -u machine:YOUR_PASSWORD
|
||||
```
|
||||
|
||||
### Example response
|
||||
@@ -119,7 +159,7 @@ To receive continuous updates (via WebSockets), subscribe to the machine namespa
|
||||
### Example request (mock machine)
|
||||
|
||||
```bash
|
||||
curl -X GET "http://10.10.10.1:3001/api/v2/machine/mock/57922"
|
||||
curl -X GET "https://IP_ADDRESS/api/v2/machine/mock/57922" -u machine:YOUR_PASSWORD
|
||||
```
|
||||
|
||||
### Example response
|
||||
@@ -174,7 +214,7 @@ The API does **not** return the newly-applied state in the POST response. The pa
|
||||
curl -X POST \
|
||||
-d \
|
||||
-H "Content-Type: application/json" \
|
||||
"http://10.10.10.1:3001/api/v1/machine/mock/57922"
|
||||
"https://IP_ADDRESS/api/v1/machine/mock/57922" -u machine:YOUR_PASSWORD
|
||||
```
|
||||
|
||||
### Example response
|
||||
|
||||
Generated
+5
-22
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Generated
+111
-12
@@ -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",
|
||||
|
||||
@@ -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"; };
|
||||
|
||||
@@ -27,7 +27,6 @@ impl MachineAct for AnalogInputTestMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ impl MachineAct for AquaPathV1 {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ impl MachineAct for BufferV1 {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ impl MachineAct for DigitalInputTestMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@ impl MachineAct for ExtruderV2 {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,6 @@ impl MachineAct for ExtruderV2 {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,6 @@ impl MachineAct for ExtruderV3 {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ impl MachineAct for ExtruderV2 {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ impl MachineAct for IP20TestMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
|
||||
@@ -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)
|
||||
|
||||
+7
-29
@@ -26,7 +26,6 @@ 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;
|
||||
@@ -39,11 +38,6 @@ 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 +75,6 @@ pub struct CrossConnection {
|
||||
pub enum AsyncThreadMessage {
|
||||
NoMsg,
|
||||
ConnectOneWayRequest(CrossConnection),
|
||||
ConnectTwoWayRequest(CrossConnection),
|
||||
DisconnectMachines(CrossConnection),
|
||||
}
|
||||
|
||||
@@ -314,7 +307,6 @@ pub enum MachineMessage {
|
||||
ConnectToMachine(MachineConnection),
|
||||
DisconnectMachine(MachineConnection),
|
||||
RequestValues(Sender<MachineValues>),
|
||||
ReceiveLiveValues(MachinesLiveValues),
|
||||
}
|
||||
|
||||
pub trait MachineApi {
|
||||
@@ -490,10 +482,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 +511,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 +535,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 +552,6 @@ where
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(live_values) =>
|
||||
{
|
||||
self.on_receive_live_values(live_values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
use crate::laser::api::LiveValuesEvent as LaserLiveValues;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MachinesLiveValues {
|
||||
Laser(LaserLiveValues),
|
||||
}
|
||||
@@ -68,7 +68,6 @@ impl MachineAct for MockMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ impl MachineAct for TestMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ impl MachineAct for TestMachineStepper {
|
||||
MachineMessage::ConnectToMachine(_machine_connection) => {}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) => {}
|
||||
MachineMessage::RequestValues(_sender) => {}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug,Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum Direction
|
||||
{
|
||||
Forward,
|
||||
Reverse,
|
||||
}
|
||||
|
||||
impl Direction
|
||||
{
|
||||
pub fn from_bool(forward: bool) -> Self
|
||||
{
|
||||
match forward
|
||||
{
|
||||
true => Self::Forward,
|
||||
false => Self::Reverse,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_forward(self) -> bool
|
||||
{
|
||||
self == Self::Forward
|
||||
}
|
||||
|
||||
pub fn is_reverse(self) -> bool
|
||||
{
|
||||
self != Self::Forward
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ impl MachineAct for Wago8chDigitalIOTestMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ impl MachineAct for WagoAiTestMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ impl MachineAct for WagoDOTestMachine {
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
MachineMessage::ReceiveLiveValues(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use super::Winder2;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{MachineAct, MachineMessage, MachineValues};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineAct for Winder2 {
|
||||
fn act(&mut self, now: Instant) {
|
||||
let machine_message = self.api_receiver.try_recv();
|
||||
match machine_message {
|
||||
Ok(machine_message) => self.act_machine_message(machine_message),
|
||||
Err(_e) => (),
|
||||
};
|
||||
// sync the spool speed
|
||||
self.sync_spool_speed(now);
|
||||
|
||||
// sync the puller speed
|
||||
self.sync_puller_speed(now);
|
||||
|
||||
// sync the traverse speed
|
||||
self.sync_traverse_speed();
|
||||
|
||||
// automatically stops or pulls after N Meters if enabled
|
||||
self.stop_or_pull_spool(now);
|
||||
|
||||
if self.traverse_controller.did_change_state() {
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
// 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();
|
||||
self.last_measurement_emit = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn act_machine_message(&mut self, msg: MachineMessage) {
|
||||
match msg {
|
||||
MachineMessage::SubscribeNamespace(namespace) => {
|
||||
self.namespace.namespace = Some(namespace);
|
||||
self.emit_state();
|
||||
}
|
||||
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
use crate::MachineApi;
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(machine_connection) => {
|
||||
if self.connected_machines.len() >= self.max_connected_machines {
|
||||
tracing::debug!(
|
||||
"Refusing to add Machine Connection {:?}, since self.connected_machines would be over the limit of {:?}",
|
||||
machine_connection,
|
||||
self.max_connected_machines
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.connected_machines.push(machine_connection);
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) => {
|
||||
self.connected_machines.clear();
|
||||
}
|
||||
MachineMessage::RequestValues(sender) => {
|
||||
sender
|
||||
.send_blocking(MachineValues {
|
||||
state: serde_json::to_value(self.build_state_event())
|
||||
.expect("Failed to serialize state"),
|
||||
live_values: serde_json::to_value(self.get_live_values())
|
||||
.expect("Failed to serialize live values"),
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+243
-149
@@ -1,3 +1,9 @@
|
||||
use crate::winder2::{
|
||||
clamp_revolution::clamp_revolution_uom, filament_tension::FilamentTensionCalculator,
|
||||
puller_speed_controller::PullerSpeedController,
|
||||
};
|
||||
|
||||
use super::{clamp_revolution::Clamping, tension_arm::TensionArm};
|
||||
use control_core::{
|
||||
controllers::first_degree_motion::angular_acceleration_speed_controller::AngularAccelerationSpeedController,
|
||||
helpers::{interpolation::scale, moving_time_window::MovingTimeWindow},
|
||||
@@ -12,15 +18,15 @@ use units::f64::*;
|
||||
use units::length::{centimeter, meter};
|
||||
use units::velocity::meter_per_second;
|
||||
|
||||
use crate::{helpers::{Clamp, clamp_revolution_uom}, winder2::devices::spool::speed_controller::SpeedController};
|
||||
use crate::winder2::devices::{Puller, TensionArm};
|
||||
|
||||
use super::helpers::FilamentTensionCalculator;
|
||||
|
||||
/// Adaptive spool speed controller that automatically adjusts to maintain optimal filament tension.
|
||||
///
|
||||
/// This controller monitors filament tension via the tension arm and learns the appropriate
|
||||
/// maximum speed based on puller speed and tension feedback. It uses closed-loop control
|
||||
/// to minimize tension error and applies smooth acceleration to prevent sudden motor commands.
|
||||
#[derive(Debug)]
|
||||
pub struct AdaptiveSpeedController {
|
||||
pub struct AdaptiveSpoolSpeedController {
|
||||
/// Last commanded angular velocity sent to the spool motor
|
||||
speed: AngularVelocity,
|
||||
last_speed: AngularVelocity,
|
||||
/// Whether the speed controller is enabled (false = always returns zero speed)
|
||||
enabled: bool,
|
||||
/// Acceleration controller to smooth speed transitions and prevent sudden changes
|
||||
@@ -46,9 +52,13 @@ pub struct AdaptiveSpeedController {
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
}
|
||||
|
||||
// Constants
|
||||
impl AdaptiveSpeedController
|
||||
{
|
||||
impl Default for AdaptiveSpoolSpeedController {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AdaptiveSpoolSpeedController {
|
||||
/// Maximum speed limit (in RPM) used to initialize the acceleration controller
|
||||
const INITIAL_MAX_SPEED_RPM: f64 = 150.0;
|
||||
|
||||
@@ -77,7 +87,7 @@ impl AdaptiveSpeedController
|
||||
|
||||
const FACTOR_MAX: f64 = 20.0;
|
||||
|
||||
/// if the tension is the lowest, the speed can be up to X the puller speed
|
||||
/// if the tension is the lowest, the speed can be up to 2x the puller speed
|
||||
const MAX_SPEED_MULTIPLIER: f64 = 4.0;
|
||||
|
||||
/// Base acceleration as a fraction of max possible speed (per second)
|
||||
@@ -88,67 +98,18 @@ impl AdaptiveSpeedController
|
||||
|
||||
/// Minimum acceleration limit to prevent completely frozen motion
|
||||
const MIN_ACCELERATION_LIMIT: f64 = 0.5; // rad/s²
|
||||
}
|
||||
|
||||
// getter + setter
|
||||
impl AdaptiveSpeedController
|
||||
{
|
||||
// Getters and setters for the new configurable parameters
|
||||
pub const fn tension_target(&self) -> f64 {
|
||||
self.tension_target
|
||||
}
|
||||
|
||||
pub const fn set_tension_target(&mut self, value: f64) {
|
||||
self.tension_target = value.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
pub const fn radius_learning_rate(&self) -> f64 {
|
||||
self.radius_learning_rate
|
||||
}
|
||||
|
||||
pub const fn set_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.radius_learning_rate = radius_learning_rate.max(0.0);
|
||||
}
|
||||
|
||||
pub const fn max_speed_multiplier(&self) -> f64 {
|
||||
self.max_speed_multiplier
|
||||
}
|
||||
|
||||
pub const fn set_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.max_speed_multiplier = max_speed_multiplier.max(0.1);
|
||||
}
|
||||
|
||||
pub const fn acceleration_factor(&self) -> f64 {
|
||||
self.acceleration_factor
|
||||
}
|
||||
|
||||
pub const fn set_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.acceleration_factor = acceleration_factor.clamp(0.01, 1.0);
|
||||
}
|
||||
|
||||
pub const fn deacceleration_urgency_multiplier(&self) -> f64 {
|
||||
self.deacceleration_urgency_multiplier
|
||||
}
|
||||
|
||||
pub const fn set_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.deacceleration_urgency_multiplier = deacceleration_urgency_multiplier.max(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl AdaptiveSpeedController
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
use revolution_per_minute as rpm;
|
||||
|
||||
let max_speed = AngularVelocity::new::<rpm>(Self::INITIAL_MAX_SPEED_RPM);
|
||||
/// Creates a new adaptive spool speed controller with default settings.
|
||||
///
|
||||
/// Initializes all internal components and sets baseline parameters.
|
||||
///
|
||||
/// # Returns
|
||||
/// A new `AdaptiveSpoolSpeedController` instance ready for use.
|
||||
pub fn new() -> Self {
|
||||
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::INITIAL_MAX_SPEED_RPM);
|
||||
|
||||
Self {
|
||||
speed: AngularVelocity::ZERO,
|
||||
last_speed: AngularVelocity::ZERO,
|
||||
enabled: false,
|
||||
acceleration_controller: AngularAccelerationSpeedController::new(
|
||||
Some(AngularVelocity::ZERO),
|
||||
@@ -174,36 +135,71 @@ impl AdaptiveSpeedController
|
||||
deacceleration_urgency_multiplier: Self::DEACCELERATION_URGENCY_MULTIPLIER,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
impl AdaptiveSpeedController
|
||||
{
|
||||
/// Calculates the current maximum speed based on puller speed and learned factor.
|
||||
///
|
||||
/// Uses the fundamental relationship between linear and angular velocity:
|
||||
/// ω = (v / r) × multiplier
|
||||
///
|
||||
/// Where:
|
||||
/// - ω (omega) = angular velocity in radians per second
|
||||
/// - v = linear velocity in meters per second
|
||||
/// - r = radius in meters
|
||||
/// - multiplier = MAX_SPEED_MULTIPLIER for speed scaling
|
||||
///
|
||||
/// This ensures the spool's rim speed is properly scaled relative to the puller's
|
||||
/// linear speed, with the multiplier allowing for tension control adjustments.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `puller_speed_controller`: Reference to puller for baseline speed calculation
|
||||
///
|
||||
/// # Returns
|
||||
/// Current maximum angular velocity for the spool in radians per second
|
||||
fn get_max_speed(&self, puller_speed_controller: &PullerSpeedController) -> AngularVelocity {
|
||||
AngularVelocity::new::<radian_per_second>(
|
||||
(puller_speed_controller.last_speed.get::<meter_per_second>()
|
||||
/ self.speed_factor.get::<meter>())
|
||||
* self.max_speed_multiplier,
|
||||
)
|
||||
}
|
||||
|
||||
/// Calculates the desired spool speed based on filament tension feedback.
|
||||
///
|
||||
/// Reads the tension arm angle, converts it to normalized tension, and calculates
|
||||
/// the appropriate speed. Also updates the learned maximum speed based on tension error.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `t`: Current timestamp for learning updates
|
||||
/// - `tension_arm`: Reference to tension arm for angle measurements
|
||||
/// - `puller_speed_controller`: Reference to puller for baseline speed calculations
|
||||
///
|
||||
/// # Returns
|
||||
/// The calculated target angular velocity for the spool motor
|
||||
fn calculate_speed(
|
||||
&mut self,
|
||||
t: Instant,
|
||||
tension_arm: &TensionArm,
|
||||
puller: &Puller,
|
||||
puller_speed_controller: &PullerSpeedController,
|
||||
) -> AngularVelocity {
|
||||
let min_speed = AngularVelocity::ZERO;
|
||||
let max_speed = self.max_speed(puller).abs();
|
||||
let max_speed = self.get_max_speed(puller_speed_controller).abs();
|
||||
|
||||
// Calculate filament tension from arm angle
|
||||
let tension_arm_revolution = clamp_revolution_uom(
|
||||
tension_arm.get_angle(),
|
||||
let tension_arm_angle = tension_arm.get_angle();
|
||||
let (clamped_angle, clamping_state) = clamp_revolution_uom(
|
||||
tension_arm_angle,
|
||||
self.filament_calc.get_max_angle(), // Inverted because min angle = max tension
|
||||
self.filament_calc.get_min_angle(),
|
||||
);
|
||||
|
||||
// Return minimum speed if tension arm is at limits
|
||||
if matches!(tension_arm_revolution.clamp, Clamp::Min | Clamp::Max)
|
||||
{
|
||||
if matches!(clamping_state, Clamping::Min | Clamping::Max) {
|
||||
return min_speed;
|
||||
}
|
||||
|
||||
// 1.0 means maximum tension (high angle, low speed)
|
||||
// 0.0 means minimum tension (low angle, high speed)
|
||||
let filament_tension = self.filament_calc.calc_filament_tension(tension_arm_revolution.value);
|
||||
let filament_tension = self.filament_calc.calc_filament_tension(clamped_angle);
|
||||
|
||||
self.update_speed_factor(filament_tension, t);
|
||||
|
||||
@@ -216,15 +212,27 @@ impl AdaptiveSpeedController
|
||||
))
|
||||
}
|
||||
|
||||
/// Simplified urgency-weighted acceleration that adapts to current operating conditions.
|
||||
///
|
||||
/// This approach uses the current max speed (calculated from puller speed and learned diameter)
|
||||
/// as the basis for acceleration limits, ensuring smooth operation across all speed ranges
|
||||
/// while providing instant deceleration capability when approaching zero.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `target_speed`: Target speed to accelerate towards
|
||||
/// - `puller_speed_controller`: Reference to puller for current max speed calculation
|
||||
/// - `t`: Current timestamp for the acceleration controller
|
||||
///
|
||||
/// # Returns
|
||||
/// Smoothly accelerated speed with urgency-weighted limits
|
||||
fn accelerate_speed(
|
||||
&mut self,
|
||||
target_speed: AngularVelocity,
|
||||
puller: &Puller,
|
||||
puller_speed_controller: &PullerSpeedController,
|
||||
t: Instant,
|
||||
) -> AngularVelocity
|
||||
{
|
||||
) -> AngularVelocity {
|
||||
let target_speed_rad_s = target_speed.get::<radian_per_second>();
|
||||
let current_max_speed = self.max_speed(puller);
|
||||
let current_max_speed = self.get_max_speed(puller_speed_controller);
|
||||
let max_speed_rad_s = current_max_speed.get::<radian_per_second>();
|
||||
|
||||
// Base acceleration proportional to current max operating speed
|
||||
@@ -245,31 +253,48 @@ impl AdaptiveSpeedController
|
||||
AngularAcceleration::new::<radian_per_second_squared>(acceleration_limit);
|
||||
|
||||
// Update acceleration controller limits
|
||||
self.acceleration_controller.set_max_acceleration(acceleration);
|
||||
self.acceleration_controller.set_min_acceleration(-acceleration);
|
||||
self.acceleration_controller
|
||||
.set_max_acceleration(acceleration);
|
||||
self.acceleration_controller
|
||||
.set_min_acceleration(-acceleration);
|
||||
|
||||
let new_speed = self.acceleration_controller.update(target_speed, t);
|
||||
|
||||
// Record for diagnostics
|
||||
self.speed_time_window.update(new_speed.get::<radian_per_second>(), t);
|
||||
self.speed_time_window
|
||||
.update(new_speed.get::<radian_per_second>(), t);
|
||||
|
||||
new_speed
|
||||
}
|
||||
|
||||
fn clamp_speed(&self, speed: AngularVelocity) -> AngularVelocity
|
||||
{
|
||||
use revolution_per_minute as rpm;
|
||||
|
||||
/// Safety function that enforces absolute speed limits.
|
||||
///
|
||||
/// Ensures the spool speed never exceeds safe operational limits to protect hardware.
|
||||
/// Acts as a final safety check after all other speed calculations.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `speed`: The speed to be safety-checked and clamped
|
||||
///
|
||||
/// # Returns
|
||||
/// Speed guaranteed to be within safe operational limits
|
||||
fn clamp_speed(&self, speed: AngularVelocity) -> AngularVelocity {
|
||||
let min_speed = AngularVelocity::ZERO;
|
||||
let max_speed = AngularVelocity::new::<rpm>(Self::SAFETY_MAX_SPEED_RPM);
|
||||
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::SAFETY_MAX_SPEED_RPM);
|
||||
|
||||
speed.max(min_speed).min(max_speed)
|
||||
}
|
||||
|
||||
fn update_speed_factor(&mut self, filament_tension: f64, t: Instant)
|
||||
{
|
||||
let delta_t = match self.last_max_speed_factor_update
|
||||
{
|
||||
/// Adaptive learning algorithm that adjusts maximum speed factor based on tension feedback.
|
||||
///
|
||||
/// Uses proportional control to adjust the speed factor based on tension error.
|
||||
/// The controller learns over time to maintain optimal tension by scaling the
|
||||
/// baseline speed derived from puller speed.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `filament_tension`: Normalized tension value
|
||||
/// - `t`: Current timestamp for time-aware learning rate
|
||||
fn update_speed_factor(&mut self, filament_tension: f64, t: Instant) {
|
||||
let delta_t = match self.last_max_speed_factor_update {
|
||||
Some(last_update) => t.duration_since(last_update).as_secs_f64(),
|
||||
None => {
|
||||
// First call, initialize and return early
|
||||
@@ -295,65 +320,66 @@ impl AdaptiveSpeedController
|
||||
self.last_max_speed_factor_update = Some(t);
|
||||
}
|
||||
|
||||
fn max_speed(&self, puller: &Puller) -> AngularVelocity
|
||||
{
|
||||
let puller_speed = puller.output_speed().get::<meter_per_second>();
|
||||
let speed_factor = self.speed_factor.get::<meter>();
|
||||
/// Main update function that orchestrates the complete speed control pipeline.
|
||||
///
|
||||
/// This is the primary interface that calculates the target speed, applies enable/disable
|
||||
/// logic, smooths speed changes, and applies safety limits.
|
||||
///
|
||||
/// Call this function every control loop iteration to get the commanded spool speed.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `t`: Current timestamp for time-based calculations
|
||||
/// - `tension_arm`: Reference to tension arm for feedback
|
||||
/// - `puller_speed_controller`: Reference for adaptive speed scaling
|
||||
///
|
||||
/// # Returns
|
||||
/// Final commanded angular velocity for the spool motor
|
||||
pub fn update_speed(
|
||||
&mut self,
|
||||
t: Instant,
|
||||
tension_arm: &TensionArm,
|
||||
puller_speed_controller: &PullerSpeedController,
|
||||
) -> AngularVelocity {
|
||||
let target_speed = self.calculate_speed(t, tension_arm, puller_speed_controller);
|
||||
|
||||
let speed = (puller_speed / speed_factor) * self.max_speed_multiplier;
|
||||
|
||||
AngularVelocity::new::<radian_per_second>(speed)
|
||||
}
|
||||
}
|
||||
|
||||
impl SpeedController for AdaptiveSpeedController
|
||||
{
|
||||
fn speed(&self) -> AngularVelocity
|
||||
{
|
||||
self.speed
|
||||
}
|
||||
|
||||
fn set_speed(&mut self, value: AngularVelocity)
|
||||
{
|
||||
self.speed = value;
|
||||
self.acceleration_controller.reset(value);
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool
|
||||
{
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, value: bool)
|
||||
{
|
||||
self.enabled = value;
|
||||
}
|
||||
|
||||
fn update_speed(
|
||||
&mut self,
|
||||
t: Instant,
|
||||
multiplier: f64,
|
||||
tension_arm: &TensionArm,
|
||||
puller: &Puller
|
||||
) -> AngularVelocity
|
||||
{
|
||||
let speed = match self.enabled
|
||||
{
|
||||
true => self.calculate_speed(t, tension_arm, puller),
|
||||
false => AngularVelocity::ZERO,
|
||||
let enabled_speed = if self.enabled {
|
||||
target_speed
|
||||
} else {
|
||||
AngularVelocity::ZERO
|
||||
};
|
||||
|
||||
let accelerated_speed = self.accelerate_speed(speed * multiplier, puller, t);
|
||||
|
||||
let accelerated_speed = self.accelerate_speed(enabled_speed, puller_speed_controller, t);
|
||||
|
||||
// Store speed before clamping to preserve the actual commanded value
|
||||
self.speed = accelerated_speed;
|
||||
self.last_speed = accelerated_speed;
|
||||
|
||||
self.clamp_speed(accelerated_speed)
|
||||
}
|
||||
|
||||
fn reset(&mut self)
|
||||
{
|
||||
self.speed = AngularVelocity::ZERO;
|
||||
|
||||
/// Enables or disables the speed controller.
|
||||
///
|
||||
/// When disabled, the controller returns zero speed regardless of tension feedback.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `enabled`: True to enable speed control, false to disable
|
||||
pub const fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
/// Returns whether the speed controller is currently enabled.
|
||||
///
|
||||
/// # Returns
|
||||
/// True if the controller is enabled, false if disabled
|
||||
pub const fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Resets the controller to initial state, clearing all learned parameters.
|
||||
///
|
||||
/// Use this when starting a new winding operation or after significant
|
||||
/// process changes that would invalidate learned parameters.
|
||||
pub fn reset(&mut self) {
|
||||
self.last_speed = AngularVelocity::ZERO;
|
||||
self.acceleration_controller.reset(AngularVelocity::ZERO);
|
||||
self.speed_factor = Length::new::<centimeter>(4.25);
|
||||
self.last_max_speed_factor_update = None;
|
||||
@@ -363,4 +389,72 @@ impl SpeedController for AdaptiveSpeedController
|
||||
self.acceleration_factor = Self::ACCELERATION_FACTOR;
|
||||
self.deacceleration_urgency_multiplier = Self::DEACCELERATION_URGENCY_MULTIPLIER;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the last commanded speed from the controller.
|
||||
///
|
||||
/// # Returns
|
||||
/// The angular velocity that was last commanded to the spool motor
|
||||
pub fn get_speed(&self) -> AngularVelocity {
|
||||
self.last_speed
|
||||
}
|
||||
|
||||
/// Manually sets the current speed, bypassing normal calculation.
|
||||
///
|
||||
/// Use this for manual control or when switching control modes to ensure
|
||||
/// smooth transitions.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `speed`: The angular velocity to set as the current speed
|
||||
pub fn set_speed(&mut self, speed: AngularVelocity) {
|
||||
self.last_speed = speed;
|
||||
self.acceleration_controller.reset(speed);
|
||||
}
|
||||
|
||||
pub fn get_speed_factor(&self) -> Length {
|
||||
self.speed_factor
|
||||
}
|
||||
|
||||
// Getters and setters for the new configurable parameters
|
||||
pub const fn get_tension_target(&self) -> f64 {
|
||||
self.tension_target
|
||||
}
|
||||
|
||||
pub const fn set_tension_target(&mut self, tension_target: f64) {
|
||||
self.tension_target = tension_target.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
pub const fn get_radius_learning_rate(&self) -> f64 {
|
||||
self.radius_learning_rate
|
||||
}
|
||||
|
||||
pub const fn set_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.radius_learning_rate = radius_learning_rate.max(0.0);
|
||||
}
|
||||
|
||||
pub const fn get_max_speed_multiplier(&self) -> f64 {
|
||||
self.max_speed_multiplier
|
||||
}
|
||||
|
||||
pub const fn set_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.max_speed_multiplier = max_speed_multiplier.max(0.1);
|
||||
}
|
||||
|
||||
pub const fn get_acceleration_factor(&self) -> f64 {
|
||||
self.acceleration_factor
|
||||
}
|
||||
|
||||
pub const fn set_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.acceleration_factor = acceleration_factor.clamp(0.01, 1.0);
|
||||
}
|
||||
|
||||
pub const fn get_deacceleration_urgency_multiplier(&self) -> f64 {
|
||||
self.deacceleration_urgency_multiplier
|
||||
}
|
||||
|
||||
pub const fn set_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.deacceleration_urgency_multiplier = deacceleration_urgency_multiplier.max(1.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
#[cfg(feature = "mock-machine")]
|
||||
mod winder2_imports {
|
||||
pub use super::super::Winder2Mode;
|
||||
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
|
||||
pub use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
|
||||
pub use control_core_derive::BuildEvent;
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use serde_json::Value;
|
||||
pub use smol::lock::Mutex;
|
||||
pub use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use tracing::instrument;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
mod winder2_imports {
|
||||
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
|
||||
pub use super::super::{Winder2, Winder2Mode};
|
||||
pub use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
|
||||
pub use control_core_derive::BuildEvent;
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use serde_json::Value;
|
||||
pub use smol::lock::Mutex;
|
||||
pub use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use tracing::instrument;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use smol::channel::Sender;
|
||||
pub use winder2_imports::*;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{MachineApi, MachineMessage};
|
||||
use crate::{MachineCrossConnectionState, machine_identification::MachineIdentificationUnique};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Standby,
|
||||
Hold,
|
||||
Pull,
|
||||
Wind,
|
||||
}
|
||||
|
||||
impl From<Winder2Mode> for Mode {
|
||||
fn from(mode: Winder2Mode) -> Self {
|
||||
match mode {
|
||||
Winder2Mode::Standby => Self::Standby,
|
||||
Winder2Mode::Hold => Self::Hold,
|
||||
Winder2Mode::Pull => Self::Pull,
|
||||
Winder2Mode::Wind => Self::Wind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mode> for Winder2Mode {
|
||||
fn from(mode: Mode) -> Self {
|
||||
match mode {
|
||||
Mode::Standby => Self::Standby,
|
||||
Mode::Hold => Self::Hold,
|
||||
Mode::Pull => Self::Pull,
|
||||
Mode::Wind => Self::Wind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum Mutation {
|
||||
// Traverse
|
||||
/// Position in mm from home point
|
||||
SetTraverseLimitOuter(f64),
|
||||
/// Position in mm from home point
|
||||
SetTraverseLimitInner(f64),
|
||||
/// Step size in mm for traverse movement
|
||||
SetTraverseStepSize(f64),
|
||||
/// Padding in mm for traverse movement limits
|
||||
SetTraversePadding(f64),
|
||||
GotoTraverseLimitOuter,
|
||||
GotoTraverseLimitInner,
|
||||
/// Find home point
|
||||
GotoTraverseHome,
|
||||
EnableTraverseLaserpointer(bool),
|
||||
|
||||
// Puller
|
||||
/// on = speed, off = stop
|
||||
SetPullerRegulationMode(PullerRegulationMode),
|
||||
SetPullerTargetSpeed(f64),
|
||||
SetPullerTargetDiameter(f64),
|
||||
SetPullerForward(bool),
|
||||
SetPullerGearRatio(GearRatio),
|
||||
|
||||
// Spool Speed Controller
|
||||
SetSpoolRegulationMode(super::spool_speed_controller::SpoolSpeedControllerType),
|
||||
SetSpoolMinMaxMinSpeed(f64),
|
||||
SetSpoolMinMaxMaxSpeed(f64),
|
||||
SetSpoolForward(bool),
|
||||
|
||||
// Adaptive Spool Speed Controller Parameters
|
||||
SetSpoolAdaptiveTensionTarget(f64),
|
||||
SetSpoolAdaptiveRadiusLearningRate(f64),
|
||||
SetSpoolAdaptiveMaxSpeedMultiplier(f64),
|
||||
SetSpoolAdaptiveAccelerationFactor(f64),
|
||||
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(f64),
|
||||
|
||||
// Spool Auto Stop/Pull
|
||||
SetSpoolAutomaticRequiredMeters(f64),
|
||||
SetSpoolAutomaticAction(SpoolAutomaticActionMode),
|
||||
ResetSpoolProgress,
|
||||
|
||||
// Tension Arm
|
||||
ZeroTensionArmAngle,
|
||||
|
||||
// Mode
|
||||
SetMode(Mode),
|
||||
|
||||
// Connected Machine
|
||||
SetConnectedMachine(MachineIdentificationUnique),
|
||||
|
||||
// Disconnect Machine
|
||||
DisconnectMachine(MachineIdentificationUnique),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct LiveValuesEvent {
|
||||
/// traverse position in mm
|
||||
pub traverse_position: Option<f64>,
|
||||
/// puller speed in m/min
|
||||
pub puller_speed: f64,
|
||||
/// spool rpm
|
||||
pub spool_rpm: f64,
|
||||
/// tension arm angle in degrees
|
||||
pub tension_arm_angle: f64,
|
||||
// spool progress in meters (pulled distance of filament)
|
||||
pub spool_progress: f64,
|
||||
}
|
||||
|
||||
impl LiveValuesEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("LiveValuesEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, BuildEvent)]
|
||||
pub struct StateEvent {
|
||||
pub is_default_state: bool,
|
||||
/// traverse state
|
||||
pub traverse_state: TraverseState,
|
||||
/// puller state
|
||||
pub puller_state: PullerState,
|
||||
/// spool automatic action state and progress
|
||||
pub spool_automatic_action_state: SpoolAutomaticActionState,
|
||||
/// mode state
|
||||
pub mode_state: ModeState,
|
||||
/// tension arm state
|
||||
pub tension_arm_state: TensionArmState,
|
||||
/// spool speed controller state
|
||||
pub spool_speed_controller_state: SpoolSpeedControllerState,
|
||||
/// Is a Machine Connected?
|
||||
pub connected_machine_state: MachineCrossConnectionState,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct TraverseState {
|
||||
/// min position in mm
|
||||
pub limit_inner: f64,
|
||||
/// max position in mm
|
||||
pub limit_outer: f64,
|
||||
/// position in mm
|
||||
pub position_in: f64,
|
||||
/// position out in mm
|
||||
pub position_out: f64,
|
||||
/// is going to position in
|
||||
pub is_going_in: bool,
|
||||
/// is going to position out
|
||||
pub is_going_out: bool,
|
||||
/// if is homed
|
||||
pub is_homed: bool,
|
||||
/// if is homing
|
||||
pub is_going_home: bool,
|
||||
/// if is traversing
|
||||
pub is_traversing: bool,
|
||||
/// laserpointer is on
|
||||
pub laserpointer: bool,
|
||||
/// step size in mm
|
||||
pub step_size: f64,
|
||||
/// padding in mm
|
||||
pub padding: f64,
|
||||
/// can go in (to inner limit)
|
||||
pub can_go_in: bool,
|
||||
/// can go out (to outer limit)
|
||||
pub can_go_out: bool,
|
||||
/// can home
|
||||
pub can_go_home: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct PullerState {
|
||||
/// regulation type
|
||||
pub regulation: PullerRegulationMode,
|
||||
/// target speed in m/min
|
||||
pub target_speed: f64,
|
||||
/// target diameter in mm
|
||||
pub target_diameter: f64,
|
||||
/// forward rotation direction
|
||||
pub forward: bool,
|
||||
/// gear ratio for winding speed
|
||||
pub gear_ratio: GearRatio,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
|
||||
pub enum SpoolAutomaticActionMode {
|
||||
#[default]
|
||||
NoAction,
|
||||
Pull,
|
||||
Hold,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct SpoolAutomaticActionState {
|
||||
pub spool_required_meters: f64,
|
||||
pub spool_automatic_action_mode: SpoolAutomaticActionMode,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct ModeState {
|
||||
/// mode
|
||||
pub mode: Mode,
|
||||
/// can wind
|
||||
pub can_wind: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct TensionArmState {
|
||||
/// is zeroed
|
||||
pub zeroed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct SpoolSpeedControllerState {
|
||||
/// regulation mode
|
||||
pub regulation_mode: super::spool_speed_controller::SpoolSpeedControllerType,
|
||||
/// min speed in rpm for minmax mode
|
||||
pub minmax_min_speed: f64,
|
||||
/// max speed in rpm for minmax mode
|
||||
pub minmax_max_speed: f64,
|
||||
/// tension target for adaptive mode (0.0-1.0)
|
||||
pub adaptive_tension_target: f64,
|
||||
/// radius learning rate for adaptive mode
|
||||
pub adaptive_radius_learning_rate: f64,
|
||||
/// max speed multiplier for adaptive mode
|
||||
pub adaptive_max_speed_multiplier: f64,
|
||||
/// acceleration factor for adaptive mode
|
||||
pub adaptive_acceleration_factor: f64,
|
||||
/// deacceleration urgency multiplier for adaptive mode
|
||||
pub adaptive_deacceleration_urgency_multiplier: f64,
|
||||
/// forward rotation direction
|
||||
pub forward: bool,
|
||||
}
|
||||
|
||||
pub enum Winder2Events {
|
||||
LiveValues(Event<LiveValuesEvent>),
|
||||
State(Event<StateEvent>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Winder2Namespace {
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<Winder2Events> for Winder2Namespace {
|
||||
#[instrument(skip_all)]
|
||||
fn emit(&mut self, events: Winder2Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
match &mut self.namespace {
|
||||
Some(ns) => ns.emit(event, &buffer_fn),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheableEvents<Self> for Winder2Events {
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
match self {
|
||||
Self::LiveValues(event) => event.into(),
|
||||
Self::State(event) => event.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
let cache_first_and_last = cache_first_and_last_event();
|
||||
match self {
|
||||
Self::LiveValues(_) => cache_first_and_last,
|
||||
Self::State(_) => cache_first_and_last,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineApi for Winder2 {
|
||||
fn api_get_sender(&self) -> Sender<MachineMessage> {
|
||||
self.api_sender.clone()
|
||||
}
|
||||
|
||||
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
|
||||
use crate::Machine;
|
||||
|
||||
let mutation: Mutation = serde_json::from_value(request_body)?;
|
||||
match mutation {
|
||||
Mutation::EnableTraverseLaserpointer(enable) => self.set_laser(enable),
|
||||
Mutation::SetMode(mode) => self.set_mode(&mode.into()),
|
||||
Mutation::SetTraverseLimitOuter(limit) => self.traverse_set_limit_outer(limit),
|
||||
Mutation::SetTraverseLimitInner(limit) => self.traverse_set_limit_inner(limit),
|
||||
Mutation::SetTraverseStepSize(size) => self.traverse_set_step_size(size),
|
||||
Mutation::SetTraversePadding(padding) => self.traverse_set_padding(padding),
|
||||
Mutation::GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
|
||||
Mutation::GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
|
||||
Mutation::GotoTraverseHome => self.traverse_goto_home(),
|
||||
Mutation::SetPullerRegulationMode(regulation) => self.puller_set_regulation(regulation),
|
||||
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
|
||||
Mutation::SetPullerTargetDiameter(_) => todo!(),
|
||||
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
|
||||
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
|
||||
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
|
||||
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
|
||||
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
|
||||
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
|
||||
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
|
||||
self.spool_set_adaptive_tension_target(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveRadiusLearningRate(value) => {
|
||||
self.spool_set_adaptive_radius_learning_rate(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveMaxSpeedMultiplier(value) => {
|
||||
self.spool_set_adaptive_max_speed_multiplier(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveAccelerationFactor(value) => {
|
||||
self.spool_set_adaptive_acceleration_factor(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
|
||||
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
|
||||
}
|
||||
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
|
||||
self.set_spool_automatic_required_meters(meters)
|
||||
}
|
||||
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
|
||||
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
|
||||
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
|
||||
Mutation::SetConnectedMachine(machine_identification_unique) => {
|
||||
let main_sender = match &self.main_sender {
|
||||
Some(sender) => sender,
|
||||
None => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Machine cannot connect to others! {:?}",
|
||||
self.get_machine_identification_unique()
|
||||
));
|
||||
}
|
||||
};
|
||||
main_sender.try_send(crate::AsyncThreadMessage::ConnectOneWayRequest(
|
||||
crate::CrossConnection {
|
||||
src: self.get_machine_identification_unique(),
|
||||
dest: machine_identification_unique,
|
||||
},
|
||||
))?;
|
||||
self.emit_state();
|
||||
}
|
||||
Mutation::DisconnectMachine(_machine_identification_unique) => {
|
||||
self.connected_machines.clear();
|
||||
/*let main_sender = match &self.main_sender {
|
||||
Some(sender) => sender,
|
||||
None => return Err(anyhow::anyhow!("[DisconnectMachine] Machine cannot connect to others! {:?}", self.machine_identification_unique)),
|
||||
};*/
|
||||
//main_sender.try_send(crate::AsyncThreadMessage::ConnectOneWayRequest(crate::CrossConnection { src: self.get_machine_identification_unique(), dest: machine_identification_unique }))?;
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Option<Namespace> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use control_core::socketio::{event::{BuildEvent, GenericEvent}, namespace::{CacheFn, CacheableEvents, cache_duration}};
|
||||
use control_core_derive::BuildEvent;
|
||||
use serde::Serialize;
|
||||
use units::{angle::degree, angular_velocity::revolution_per_minute, length::{meter, millimeter}, velocity::meter_per_minute};
|
||||
|
||||
use crate::winder2::Winder2;
|
||||
|
||||
mod state;
|
||||
pub use state::State;
|
||||
|
||||
mod mutation;
|
||||
|
||||
// Live values
|
||||
#[derive(Serialize, Debug, Clone, Default, BuildEvent)]
|
||||
pub struct LiveValues
|
||||
{
|
||||
/// traverse position in mm
|
||||
pub traverse_position: Option<f64>,
|
||||
/// puller speed in m/min
|
||||
pub puller_speed: f64,
|
||||
/// spool rpm
|
||||
pub spool_rpm: f64,
|
||||
/// tension arm angle in degrees
|
||||
pub tension_arm_angle: f64,
|
||||
// spool progress in meters (pulled distance of filament)
|
||||
pub spool_length_task_progress: f64,
|
||||
}
|
||||
|
||||
impl CacheableEvents<Self> for LiveValues
|
||||
{
|
||||
fn event_value(&self) -> GenericEvent
|
||||
{
|
||||
self.build().into()
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1))
|
||||
}
|
||||
}
|
||||
|
||||
impl Winder2
|
||||
{
|
||||
pub fn create_live_values(&self) -> LiveValues
|
||||
{
|
||||
let tension_arm_angle = self.tension_arm.get_angle().get::<degree>();
|
||||
|
||||
// Wrap [270;<360] to [-90; 0]
|
||||
// This is done to reduce flicker in the graphs around the zero point
|
||||
let tension_arm_angle = match tension_arm_angle >= 270.0
|
||||
{
|
||||
true => tension_arm_angle - 360.0,
|
||||
false => tension_arm_angle,
|
||||
};
|
||||
|
||||
LiveValues {
|
||||
traverse_position: self.traverse.current_position().map(|x| x.get::<millimeter>()),
|
||||
puller_speed: self.puller.output_speed().get::<meter_per_minute>(),
|
||||
spool_rpm: self.spool.speed().get::<revolution_per_minute>(),
|
||||
spool_length_task_progress: self.spool_length_task.current_length().get::<meter>(),
|
||||
tension_arm_angle,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use units::{
|
||||
AngularVelocity,
|
||||
Length,
|
||||
Velocity,
|
||||
angular_velocity::revolution_per_minute,
|
||||
length::{meter, millimeter},
|
||||
velocity::meter_per_minute
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AsyncThreadMessage, CrossConnection, Machine, machine_identification::MachineIdentificationUnique, types::Direction, winder2::{Winder2, devices::{
|
||||
OperationState,
|
||||
PullerGearRatio,
|
||||
PullerSpeedControlMode,
|
||||
SpoolSpeedControlMode
|
||||
}, types::{Mode, SpoolLengthTaskCompletedAction}}
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum Mutation
|
||||
{
|
||||
// Machine
|
||||
SetMode(Mode),
|
||||
|
||||
// Spool
|
||||
SetSpoolDirection(Direction),
|
||||
SetSpoolSpeedControlMode(SpoolSpeedControlMode),
|
||||
|
||||
// Spool Speed MinMax Strategy
|
||||
SetSpoolMinMaxMinSpeed(f64),
|
||||
SetSpoolMinMaxMaxSpeed(f64),
|
||||
|
||||
// Spool Speed Adaptive Strategy
|
||||
SetSpoolAdaptiveTensionTarget(f64),
|
||||
SetSpoolAdaptiveRadiusLearningRate(f64),
|
||||
SetSpoolAdaptiveMaxSpeedMultiplier(f64),
|
||||
SetSpoolAdaptiveAccelerationFactor(f64),
|
||||
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(f64),
|
||||
|
||||
// Puller
|
||||
SetPullerDirection(Direction),
|
||||
SetPullerGearRatio(PullerGearRatio),
|
||||
SetPullerSpeedControlMode(PullerSpeedControlMode),
|
||||
|
||||
// Puller Speed Fixed Strategy
|
||||
SetPullerFixedTargetSpeed(f64),
|
||||
|
||||
// Puller Speed Strategy
|
||||
SetPullerAdaptiveBaseSpeed(f64), // in m/min
|
||||
SetPullerAdaptiveDeviationMax(f64), // in m/min
|
||||
SetPullerAdaptiveReferenceMachine(Option<MachineIdentificationUnique>),
|
||||
|
||||
// Traverse
|
||||
/// Position in mm from home point
|
||||
SetTraverseLimitInner(f64),
|
||||
/// Position in mm from home point
|
||||
SetTraverseLimitOuter(f64),
|
||||
/// Step size in mm for traverse movement
|
||||
SetTraverseStepSize(f64),
|
||||
/// Padding in mm for traverse movement limits
|
||||
SetTraversePadding(f64),
|
||||
GotoTraverseLimitInner,
|
||||
GotoTraverseLimitOuter,
|
||||
/// Find home point
|
||||
GotoTraverseHome,
|
||||
SetTraverseLaserpointerEnabled(bool),
|
||||
|
||||
// Tension Arm
|
||||
CalibrateTensionArmAngle,
|
||||
|
||||
// Spool Length Task
|
||||
SetSpoolLengthTaskTargetLength(f64),
|
||||
ResetSetSpoolLengthTaskProgress,
|
||||
SetOnSpoolLengthTaskCompletedAction(SpoolLengthTaskCompletedAction),
|
||||
}
|
||||
|
||||
impl Winder2
|
||||
{
|
||||
pub fn handle_mutation(&mut self, value: serde_json::Value) -> Result<(), anyhow::Error>
|
||||
{
|
||||
use Mutation::*;
|
||||
|
||||
let mutation: Mutation = serde_json::from_value(value)?;
|
||||
|
||||
match mutation
|
||||
{
|
||||
// machine
|
||||
SetMode(v) => self.set_mode(v),
|
||||
|
||||
// spool
|
||||
SetSpoolDirection(v) => self.spool_set_direction(v),
|
||||
SetSpoolSpeedControlMode(v) => self.spool_set_speed_control_mode(v),
|
||||
SetSpoolMinMaxMinSpeed(v) => self.spool_set_minmax_min_speed(v),
|
||||
SetSpoolMinMaxMaxSpeed(v) => self.spool_set_minmax_max_speed(v),
|
||||
SetSpoolAdaptiveTensionTarget(v) => self.spool_set_adaptive_tension_target(v),
|
||||
SetSpoolAdaptiveRadiusLearningRate(v) => self.spool_set_adaptive_radius_learning_rate(v),
|
||||
SetSpoolAdaptiveMaxSpeedMultiplier(v) => self.spool_set_adaptive_max_speed_multiplier(v),
|
||||
SetSpoolAdaptiveAccelerationFactor(v) => self.spool_set_adaptive_acceleration_factor(v),
|
||||
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(v) => self.spool_set_adaptive_deacceleration_urgency_multiplier(v),
|
||||
|
||||
//traverse
|
||||
SetTraverseLimitInner(v) => self.traverse_set_limit_inner(v),
|
||||
SetTraverseLimitOuter(v) => self.traverse_set_limit_outer(v),
|
||||
SetTraverseStepSize(v) => self.traverse_set_step_size(v),
|
||||
SetTraversePadding(v) => self.traverse_set_padding(v),
|
||||
GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
|
||||
GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
|
||||
GotoTraverseHome => self.traverse_goto_home(),
|
||||
SetTraverseLaserpointerEnabled(v) => self.traverse_set_laser_pointer_enabled(v),
|
||||
|
||||
// puller
|
||||
SetPullerDirection(v) => self.puller_set_direction(v),
|
||||
SetPullerGearRatio(v) => self.puller_set_gear_ratio(v),
|
||||
SetPullerSpeedControlMode(v) => self.puller_set_speed_control_mode(v),
|
||||
SetPullerFixedTargetSpeed(v) => self.puller_set_fixed_target_speed(v),
|
||||
SetPullerAdaptiveBaseSpeed(v) => self.puller_set_adaptive_base_speed(v),
|
||||
SetPullerAdaptiveDeviationMax(v) => self.puller_set_adaptive_deviation_max(v),
|
||||
SetPullerAdaptiveReferenceMachine(v) => self.puller_set_adaptive_reference_machine(v)?,
|
||||
|
||||
// tension arm
|
||||
CalibrateTensionArmAngle => self.tension_arm_calibrate(),
|
||||
|
||||
// spool length task
|
||||
SetSpoolLengthTaskTargetLength(v) =>
|
||||
self.spool_length_task_set_target_length(v),
|
||||
ResetSetSpoolLengthTaskProgress =>
|
||||
self.spool_length_task_reset(Instant::now()),
|
||||
SetOnSpoolLengthTaskCompletedAction(v) =>
|
||||
self.set_on_spool_length_task_complete(v),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Machine
|
||||
impl Winder2
|
||||
{
|
||||
pub fn set_mode(&mut self, mode: Mode)
|
||||
{
|
||||
let should_update = mode != Mode::Wind || self.can_wind();
|
||||
|
||||
if should_update
|
||||
{
|
||||
self.mode = mode;
|
||||
match self.mode
|
||||
{
|
||||
Mode::Standby =>
|
||||
{
|
||||
let state = OperationState::Disabled;
|
||||
self.spool.set_operation_state(state);
|
||||
self.puller.set_operation_state(state);
|
||||
self.traverse.set_operation_state(state);
|
||||
},
|
||||
Mode::Hold =>
|
||||
{
|
||||
let state = OperationState::Holding;
|
||||
self.spool.set_operation_state(state);
|
||||
self.puller.set_operation_state(state);
|
||||
self.traverse.set_operation_state(state);
|
||||
},
|
||||
Mode::Pull =>
|
||||
{
|
||||
use OperationState::*;
|
||||
self.spool.set_operation_state(Holding);
|
||||
self.puller.set_operation_state(Running);
|
||||
self.traverse.set_operation_state(Holding);
|
||||
},
|
||||
Mode::Wind =>
|
||||
{
|
||||
let state = OperationState::Running;
|
||||
self.spool.set_operation_state(state);
|
||||
self.puller.set_operation_state(state);
|
||||
self.traverse.set_operation_state(state);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
// Spool
|
||||
impl Winder2
|
||||
{
|
||||
pub fn spool_set_direction(&mut self, value: Direction)
|
||||
{
|
||||
self.spool.set_direction(value);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_speed_control_mode(&mut self, value: SpoolSpeedControlMode)
|
||||
{
|
||||
self.spool.set_speed_control_mode(value);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set minimum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64)
|
||||
{
|
||||
let min_speed = AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
|
||||
|
||||
if let Err(e) = self.spool.speed_controllers.minmax.set_min_speed(min_speed)
|
||||
{
|
||||
tracing::error!("Failed to set spool min speed: {:?}", e);
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set maximum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64)
|
||||
{
|
||||
let max_speed = AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
|
||||
|
||||
if let Err(e) = self.spool.speed_controllers.minmax.set_max_speed(max_speed)
|
||||
{
|
||||
tracing::error!("Failed to set spool max speed: {:?}", e);
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set tension target for adaptive mode (0.0-1.0)
|
||||
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64)
|
||||
{
|
||||
self.spool.speed_controllers.adaptive.set_tension_target(tension_target);
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set radius learning rate for adaptive mode
|
||||
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.spool.speed_controllers.adaptive
|
||||
.set_radius_learning_rate(radius_learning_rate);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set max speed multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.spool.speed_controllers.adaptive
|
||||
.set_max_speed_multiplier(max_speed_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set acceleration factor for adaptive mode
|
||||
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.spool.speed_controllers.adaptive
|
||||
.set_acceleration_factor(acceleration_factor);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set deacceleration urgency multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.spool.speed_controllers.adaptive
|
||||
.set_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
// Puller
|
||||
impl Winder2
|
||||
{
|
||||
pub fn puller_set_speed_control_mode(&mut self, mode: PullerSpeedControlMode)
|
||||
{
|
||||
self.puller.set_speed_control_mode(mode);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_direction(&mut self, direction: Direction)
|
||||
{
|
||||
self.puller.set_direction(direction);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_gear_ratio(&mut self, gear_ratio: PullerGearRatio)
|
||||
{
|
||||
self.puller.set_gear_ratio(gear_ratio);
|
||||
// safety measure when changing gear ratio
|
||||
self.set_mode(Mode::Standby);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// target_speed in m/min
|
||||
pub fn puller_set_fixed_target_speed(&mut self, speed: f64)
|
||||
{
|
||||
// Convert m/min to velocity
|
||||
let speed = Velocity::new::<meter_per_minute>(speed);
|
||||
self.puller.speed_controller_strategies_mut().fixed.set_target_speed(speed);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_adaptive_base_speed(&mut self, speed: f64)
|
||||
{
|
||||
let speed = Velocity::new::<meter_per_minute>(speed);
|
||||
self.puller.speed_controller_strategies_mut().adaptive.set_base_speed(speed);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_adaptive_deviation_max(&mut self, deviation_max: f64)
|
||||
{
|
||||
let speed = Velocity::new::<meter_per_minute>(deviation_max);
|
||||
self.puller.speed_controller_strategies_mut().adaptive.set_deviation_max(speed);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_adaptive_reference_machine(
|
||||
&mut self,
|
||||
machine_uid: Option<MachineIdentificationUnique>
|
||||
) -> Result<(), anyhow::Error>
|
||||
{
|
||||
match machine_uid
|
||||
{
|
||||
Some(machine_uid) =>
|
||||
{
|
||||
if let Some(connection) = &self.puller_speed_reference_machine
|
||||
{
|
||||
if connection.ident == machine_uid
|
||||
{ return Ok(()); }
|
||||
}
|
||||
|
||||
let main_sender = match &self.channel.main_sender
|
||||
{
|
||||
Some(v) => v,
|
||||
None =>
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"{:?} Failed to connect to {:?}",
|
||||
self.get_machine_identification_unique(),
|
||||
machine_uid,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
main_sender.try_send(AsyncThreadMessage::ConnectTwoWayRequest(
|
||||
CrossConnection {
|
||||
src: self.get_machine_identification_unique(),
|
||||
dest: machine_uid,
|
||||
},
|
||||
))?;
|
||||
|
||||
self.emit_state();
|
||||
},
|
||||
None =>
|
||||
{
|
||||
match self.puller_speed_reference_machine.take()
|
||||
{
|
||||
Some(connection) =>
|
||||
{
|
||||
_ = connection;
|
||||
//TODO: Tell other machine to disconnect
|
||||
|
||||
let main_sender = match &self.channel.main_sender
|
||||
{
|
||||
Some(v) => v,
|
||||
None =>
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"{:?} Failed to connect to {:?}",
|
||||
self.get_machine_identification_unique(),
|
||||
machine_uid,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
main_sender.try_send(AsyncThreadMessage::DisconnectMachines(
|
||||
CrossConnection {
|
||||
src: self.get_machine_identification_unique(),
|
||||
dest: connection.ident,
|
||||
},
|
||||
))?;
|
||||
},
|
||||
None => return Ok(()), // nothing to do
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse
|
||||
impl Winder2
|
||||
{
|
||||
pub fn traverse_set_laser_pointer_enabled(&mut self, value: bool)
|
||||
{
|
||||
self.traverse.set_laser_pointer_enabled(value);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_inner(&mut self, limit: f64)
|
||||
{
|
||||
let limit_inner = Length::new::<millimeter>(limit);
|
||||
_ = self.traverse.try_set_limit_inner(limit_inner);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_outer(&mut self, limit: f64)
|
||||
{
|
||||
let limit_outer = Length::new::<millimeter>(limit);
|
||||
_ = self.traverse.try_set_limit_outer(limit_outer);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_step_size(&mut self, step_size: f64)
|
||||
{
|
||||
let step_size = Length::new::<millimeter>(step_size);
|
||||
self.traverse.set_step_size(step_size);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_padding(&mut self, padding: f64)
|
||||
{
|
||||
let padding = Length::new::<millimeter>(padding);
|
||||
self.traverse.set_padding(padding);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_inner(&mut self)
|
||||
{
|
||||
_ = self.traverse.try_goto_limit_inner();
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_outer(&mut self)
|
||||
{
|
||||
_ = self.traverse.try_goto_limit_outer();
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_home(&mut self)
|
||||
{
|
||||
_ = self.traverse.try_goto_home();
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
// Tension Arm
|
||||
impl Winder2
|
||||
{
|
||||
pub fn tension_arm_calibrate(&mut self)
|
||||
{
|
||||
self.tension_arm.calibrate();
|
||||
self.emit_live_values(); // For angle update
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
// Spool Length Task
|
||||
impl Winder2
|
||||
{
|
||||
pub fn spool_length_task_set_target_length(&mut self, meters: f64)
|
||||
{
|
||||
let target_length = Length::new::<meter>(meters);
|
||||
self.spool_length_task.set_target_length(target_length);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_length_task_reset(&mut self, now: Instant)
|
||||
{
|
||||
self.spool_length_task.reset(now);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_on_spool_length_task_complete(&mut self, action: SpoolLengthTaskCompletedAction)
|
||||
{
|
||||
self.on_spool_length_task_complete = action;
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
use control_core::socketio::{event::{BuildEvent, GenericEvent}, namespace::{CacheFn, CacheableEvents, cache_first_and_last_event}};
|
||||
use control_core_derive::BuildEvent;
|
||||
use serde::Serialize;
|
||||
use units::{angular_velocity::revolution_per_minute, length::{meter, millimeter}, velocity::meter_per_minute};
|
||||
|
||||
use crate::{
|
||||
MachineCrossConnectionState, types::Direction, winder2::{
|
||||
Winder2,
|
||||
devices::{
|
||||
PullerGearRatio, PullerSpeedControlMode, SpoolSpeedControlMode
|
||||
}, types::{Mode, SpoolLengthTaskCompletedAction}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#[derive(Serialize, Debug, Clone, BuildEvent)]
|
||||
pub struct State
|
||||
{
|
||||
// common
|
||||
pub is_default_state: bool,
|
||||
|
||||
// machine
|
||||
pub mode: Mode,
|
||||
pub can_wind: bool,
|
||||
|
||||
// devices
|
||||
pub spool_state: SpoolState,
|
||||
pub puller_state: PullerState,
|
||||
pub traverse_state: TraverseState,
|
||||
pub tension_arm_state: TensionArmState,
|
||||
|
||||
// tasks
|
||||
pub spool_length_task_state: SpoolLengthTaskState,
|
||||
}
|
||||
|
||||
impl CacheableEvents<Self> for State
|
||||
{
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
self.build().into()
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
cache_first_and_last_event()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct SpoolState
|
||||
{
|
||||
pub direction: Direction,
|
||||
pub speed_control_mode: SpoolSpeedControlMode,
|
||||
|
||||
/// min max mode
|
||||
pub minmax_min_speed: f64, // in rpm
|
||||
pub minmax_max_speed: f64, // in rpm
|
||||
|
||||
// adaptive mode
|
||||
pub adaptive_tension_target: f64,
|
||||
pub adaptive_radius_learning_rate: f64,
|
||||
pub adaptive_max_speed_multiplier: f64,
|
||||
pub adaptive_acceleration_factor: f64,
|
||||
pub adaptive_deacceleration_urgency_multiplier: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct PullerState
|
||||
{
|
||||
pub direction: Direction,
|
||||
pub gear_ratio: PullerGearRatio,
|
||||
pub speed_control_mode: PullerSpeedControlMode,
|
||||
|
||||
// fixed speed strategy
|
||||
pub fixed_target_speed: f64, // in m/min
|
||||
|
||||
// adaptive speed strategy
|
||||
pub adaptive_base_speed: f64, // in m/min
|
||||
pub adaptive_deviation_max: f64, // in m/min
|
||||
pub adaptive_reference_machine: MachineCrossConnectionState,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct TraverseState {
|
||||
// config
|
||||
pub limit_inner: f64, /// in mm
|
||||
pub limit_outer: f64, /// in mm
|
||||
pub step_size: f64, /// in mm
|
||||
pub padding: f64, /// in mm
|
||||
|
||||
// states
|
||||
pub is_going_in: bool,
|
||||
pub is_going_out: bool,
|
||||
pub is_homed: bool,
|
||||
pub is_going_home: bool,
|
||||
pub is_traversing: bool,
|
||||
|
||||
// state transitions
|
||||
pub can_go_in: bool,
|
||||
pub can_go_out: bool,
|
||||
pub can_go_home: bool,
|
||||
|
||||
// lazeeeeeeeer
|
||||
pub laserpointer_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct TensionArmState
|
||||
{
|
||||
pub is_calibrated: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct SpoolLengthTaskState
|
||||
{
|
||||
pub target_length: f64,
|
||||
pub on_completed_action: SpoolLengthTaskCompletedAction,
|
||||
}
|
||||
|
||||
impl Winder2
|
||||
{
|
||||
pub fn create_state(&self) -> State
|
||||
{
|
||||
let is_default_state = !self.emitted_default_state;
|
||||
|
||||
State {
|
||||
is_default_state,
|
||||
mode: self.mode,
|
||||
can_wind: self.can_wind(),
|
||||
traverse_state: self.create_traverse_state(),
|
||||
puller_state: self.create_puller_state(),
|
||||
spool_length_task_state: self.create_spool_length_task_state(),
|
||||
tension_arm_state: self.create_tension_arm_state(),
|
||||
spool_state: self.create_spool_state(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_spool_state(&self) -> SpoolState
|
||||
{
|
||||
use revolution_per_minute as rpm;
|
||||
|
||||
let speed_controllers = &self.spool.speed_controllers;
|
||||
let minmax = &speed_controllers.minmax;
|
||||
let adaptive = &speed_controllers.adaptive;
|
||||
|
||||
let adaptive_deacceleration_urgency_multiplier =
|
||||
adaptive.deacceleration_urgency_multiplier();
|
||||
|
||||
SpoolState {
|
||||
direction: self.spool.direction(),
|
||||
speed_control_mode: self.spool.speed_control_mode(),
|
||||
// min max speed controller
|
||||
minmax_min_speed: minmax.min_speed().get::<rpm>(),
|
||||
minmax_max_speed: minmax.max_speed().get::<rpm>(),
|
||||
// adaptive speed controller
|
||||
adaptive_tension_target: adaptive.tension_target(),
|
||||
adaptive_radius_learning_rate: adaptive.radius_learning_rate(),
|
||||
adaptive_max_speed_multiplier: adaptive.max_speed_multiplier(),
|
||||
adaptive_acceleration_factor: adaptive.acceleration_factor(),
|
||||
adaptive_deacceleration_urgency_multiplier,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_puller_state(&self) -> PullerState
|
||||
{
|
||||
let puller = &self.puller;
|
||||
|
||||
let strategies = puller.speed_controller_strategies();
|
||||
|
||||
let fixed_target_speed =
|
||||
strategies.fixed.target_speed().get::<meter_per_minute>();
|
||||
|
||||
let adaptive_base_speed =
|
||||
strategies.adaptive.base_speed().get::<meter_per_minute>();
|
||||
|
||||
let adaptive_deviation_max =
|
||||
strategies.adaptive.deviation_max().get::<meter_per_minute>();
|
||||
|
||||
let adaptive_reference_machine = match &self.puller_speed_reference_machine
|
||||
{
|
||||
Some(connection) => MachineCrossConnectionState {
|
||||
machine_identification_unique: Some(connection.ident.clone()),
|
||||
is_available: true
|
||||
},
|
||||
None => MachineCrossConnectionState {
|
||||
machine_identification_unique: None,
|
||||
is_available: false
|
||||
},
|
||||
};
|
||||
|
||||
PullerState {
|
||||
direction: puller.direction(),
|
||||
gear_ratio: puller.gear_ratio(),
|
||||
speed_control_mode: puller.speed_control_mode(),
|
||||
fixed_target_speed,
|
||||
adaptive_base_speed,
|
||||
adaptive_deviation_max,
|
||||
adaptive_reference_machine,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_traverse_state(&self) -> TraverseState
|
||||
{
|
||||
let traverse = &self.traverse;
|
||||
|
||||
TraverseState {
|
||||
limit_inner: traverse.limit_inner().get::<millimeter>(),
|
||||
limit_outer: traverse.limit_outer().get::<millimeter>(),
|
||||
is_going_in: traverse.is_going_in(),
|
||||
is_going_out: traverse.is_going_out(),
|
||||
is_homed: traverse.is_homed(),
|
||||
is_going_home: traverse.is_going_home(),
|
||||
is_traversing: traverse.is_traversing(),
|
||||
laserpointer_enabled: traverse.laser_pointer_enabled(),
|
||||
step_size: traverse.step_size().get::<millimeter>(),
|
||||
padding: traverse.padding().get::<millimeter>(),
|
||||
can_go_in: traverse.can_goto_limit_inner(),
|
||||
can_go_out: traverse.can_goto_limit_outer(),
|
||||
can_go_home: traverse.can_go_home()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tension_arm_state(&self) -> TensionArmState
|
||||
{
|
||||
TensionArmState {
|
||||
is_calibrated: self.tension_arm.is_calibrated()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_spool_length_task_state(&self) -> SpoolLengthTaskState
|
||||
{
|
||||
SpoolLengthTaskState {
|
||||
target_length: self.spool_length_task.target_length().get::<meter>(),
|
||||
on_completed_action: self.on_spool_length_task_complete,
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -1,22 +0,0 @@
|
||||
mod spool;
|
||||
pub use spool::Spool;
|
||||
pub use spool::SpeedControlMode as SpoolSpeedControlMode;
|
||||
|
||||
mod puller;
|
||||
pub use puller::Puller;
|
||||
pub use puller::GearRatio as PullerGearRatio;
|
||||
pub use puller::SpeedControlMode as PullerSpeedControlMode;
|
||||
|
||||
mod tension_arm;
|
||||
pub use tension_arm::TensionArm;
|
||||
|
||||
mod traverse;
|
||||
pub use traverse::Traverse;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OperationState
|
||||
{
|
||||
Disabled,
|
||||
Holding,
|
||||
Running,
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use units::ConstZero;
|
||||
use units::{
|
||||
acceleration::meter_per_minute_per_second,
|
||||
f64::*,
|
||||
jerk::meter_per_minute_per_second_squared,
|
||||
length::centimeter,
|
||||
velocity::meter_per_minute,
|
||||
};
|
||||
|
||||
use meter_per_minute as velocity_unit;
|
||||
use meter_per_minute_per_second as acceleration_unit;
|
||||
use meter_per_minute_per_second_squared as jerk_unit;
|
||||
|
||||
use control_core::{ converters::linear_step_converter::LinearStepConverter };
|
||||
|
||||
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
|
||||
|
||||
use crate::types::Direction;
|
||||
|
||||
use super::OperationState;
|
||||
|
||||
mod speed_controller;
|
||||
pub use speed_controller::SpeedController;
|
||||
|
||||
pub use speed_controller::{
|
||||
Mode as SpeedControlMode,
|
||||
Strategies as SpeedControllerStrategies
|
||||
};
|
||||
|
||||
/// Represents the puller motor
|
||||
#[derive(Debug)]
|
||||
pub struct Puller
|
||||
{
|
||||
// hardware
|
||||
motor: StepperVelocityEL70x1,
|
||||
|
||||
// config
|
||||
operation_state: OperationState,
|
||||
direction: Direction,
|
||||
gear_ratio: GearRatio,
|
||||
|
||||
// misc
|
||||
speed_controller: SpeedController,
|
||||
step_converter: LinearStepConverter,
|
||||
}
|
||||
|
||||
// constants
|
||||
impl Puller
|
||||
{
|
||||
// fixed
|
||||
const STEPS_PER_REVOLUTION: i16 = 200; // in steps
|
||||
const WHEEL_DIAMETER: f64 = 8.0; // in cm
|
||||
const MOTOR_SPEED_MAX: f64 = 50.0; // in meters per minute
|
||||
const MOTOR_ACCELERATION_MAX: f64 = 5.0; // in meters per minute per second
|
||||
const MOTOR_JERK_MAX: f64 = 10.0; // in meters per minute per second squared
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl Puller
|
||||
{
|
||||
pub fn new(motor: StepperVelocityEL70x1) -> Self
|
||||
{
|
||||
let speed_controller = {
|
||||
let speed_max = Velocity::new::<velocity_unit>(Self::MOTOR_SPEED_MAX);
|
||||
let acc_max = Acceleration::new::<acceleration_unit>(Self::MOTOR_ACCELERATION_MAX);
|
||||
let jerk_max = Jerk::new::<jerk_unit>(Self::MOTOR_JERK_MAX);
|
||||
SpeedController::new(speed_max, jerk_max, acc_max)
|
||||
};
|
||||
|
||||
let step_converter = {
|
||||
let diameter = Length::new::<centimeter>(Self::WHEEL_DIAMETER);
|
||||
LinearStepConverter::from_diameter(Self::STEPS_PER_REVOLUTION, diameter)
|
||||
};
|
||||
|
||||
Self {
|
||||
// config
|
||||
operation_state: OperationState::Disabled,
|
||||
direction: Direction::Forward,
|
||||
gear_ratio: GearRatio::OneToOne,
|
||||
|
||||
// misc
|
||||
motor,
|
||||
speed_controller,
|
||||
step_converter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, t: Instant)
|
||||
{
|
||||
// update speed
|
||||
self.speed_controller.update(t);
|
||||
|
||||
// retrieve speed
|
||||
let speed = self.speed_controller.speed();
|
||||
|
||||
// only disable motor once it reaches a really low speed
|
||||
// to prevent an abrupt stop
|
||||
if self.operation_state == OperationState::Disabled
|
||||
&& speed.get::<meter_per_minute>() <= 0.1
|
||||
{
|
||||
self.motor.set_enabled(false);
|
||||
}
|
||||
|
||||
// convert to steps/sec
|
||||
let steps_per_second = self.step_converter.velocity_to_steps(speed);
|
||||
|
||||
// write to hardware
|
||||
_ = self.motor.set_speed(steps_per_second);
|
||||
}
|
||||
}
|
||||
|
||||
// getter + setter
|
||||
impl Puller
|
||||
{
|
||||
#[allow(dead_code)]
|
||||
pub fn operation_state(&self) -> OperationState
|
||||
{
|
||||
self.operation_state
|
||||
}
|
||||
|
||||
pub fn set_operation_state(&mut self, operation_state: OperationState)
|
||||
{
|
||||
use OperationState::*;
|
||||
|
||||
// No change, nothing to do
|
||||
if self.operation_state == operation_state { return; }
|
||||
|
||||
// Leaving disabled state, enable motor
|
||||
if self.operation_state == Disabled {
|
||||
self.motor.set_enabled(true);
|
||||
}
|
||||
|
||||
self.speed_controller.set_enabled(operation_state == Running);
|
||||
self.operation_state = operation_state;
|
||||
}
|
||||
|
||||
pub fn direction(&self) -> Direction
|
||||
{
|
||||
self.direction
|
||||
}
|
||||
|
||||
pub fn set_direction(&mut self, direction: Direction)
|
||||
{
|
||||
self.direction = direction;
|
||||
self.update_multiplier();
|
||||
}
|
||||
|
||||
pub fn gear_ratio(&self) -> GearRatio
|
||||
{
|
||||
self.gear_ratio
|
||||
}
|
||||
|
||||
pub fn set_gear_ratio(&mut self, gear_ratio: GearRatio)
|
||||
{
|
||||
self.gear_ratio = gear_ratio;
|
||||
self.update_multiplier();
|
||||
|
||||
// reset configured speeds when gear ratio changes
|
||||
// for added safety, since changing gear ratio should
|
||||
// require a reconfiguration of the machine
|
||||
let strategies = self.speed_controller.strategies_mut();
|
||||
strategies.fixed.set_target_speed(Velocity::ZERO);
|
||||
strategies.adaptive.set_base_speed(Velocity::ZERO);
|
||||
strategies.adaptive.set_deviation_max(Velocity::ZERO);
|
||||
}
|
||||
|
||||
pub fn output_speed(&self) -> Velocity
|
||||
{
|
||||
self.speed_controller.speed() / self.gear_ratio.multiplier()
|
||||
}
|
||||
|
||||
pub fn speed_control_mode(&self) -> SpeedControlMode
|
||||
{
|
||||
self.speed_controller.mode()
|
||||
}
|
||||
|
||||
pub fn set_speed_control_mode(&mut self, mode: SpeedControlMode)
|
||||
{
|
||||
self.speed_controller.set_mode(mode);
|
||||
}
|
||||
|
||||
pub fn speed_controller_strategies(&self) -> &SpeedControllerStrategies
|
||||
{
|
||||
self.speed_controller.strategies()
|
||||
}
|
||||
|
||||
pub fn speed_controller_strategies_mut(&mut self) -> &mut SpeedControllerStrategies
|
||||
{
|
||||
self.speed_controller.strategies_mut()
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
impl Puller
|
||||
{
|
||||
fn update_multiplier(&mut self)
|
||||
{
|
||||
let multiplier = self.gear_ratio.multiplier();
|
||||
let multiplier = match self.direction
|
||||
{
|
||||
Direction::Forward => multiplier,
|
||||
Direction::Reverse => -multiplier,
|
||||
};
|
||||
|
||||
self.speed_controller.set_multiplier(multiplier);
|
||||
}
|
||||
}
|
||||
|
||||
// other types
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum GearRatio
|
||||
{
|
||||
OneToOne,
|
||||
FiveToOne,
|
||||
TenToOne,
|
||||
}
|
||||
|
||||
impl GearRatio
|
||||
{
|
||||
/// Get the speed multiplier for this gear ratio
|
||||
pub fn multiplier(&self) -> f64
|
||||
{
|
||||
match self
|
||||
{
|
||||
GearRatio::OneToOne => 1.0,
|
||||
GearRatio::FiveToOne => 5.0,
|
||||
GearRatio::TenToOne => 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use units::{Acceleration, ConstZero, Jerk, Velocity};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Mode
|
||||
{
|
||||
Fixed,
|
||||
Adaptive,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpeedController
|
||||
{
|
||||
// config
|
||||
mode: Mode,
|
||||
enabled: bool,
|
||||
multiplier: f64,
|
||||
|
||||
// state
|
||||
speed: Velocity,
|
||||
|
||||
strategies: Strategies,
|
||||
acceleration_controller: LinearJerkSpeedController,
|
||||
}
|
||||
|
||||
impl SpeedController
|
||||
{
|
||||
pub fn new(speed_max: Velocity, jerk_max: Jerk, acceleration_max: Acceleration) -> Self
|
||||
{
|
||||
let fixed = FixedSpeedStrategy {
|
||||
target_speed: Velocity::ZERO
|
||||
};
|
||||
|
||||
let adaptive = AdaptiveSpeedStrategy {
|
||||
base_speed: Velocity::ZERO,
|
||||
deviation_max: Velocity::ZERO,
|
||||
modulation: 0.0,
|
||||
};
|
||||
|
||||
let acceleration_controller = LinearJerkSpeedController::new_simple(
|
||||
Some(speed_max),
|
||||
acceleration_max,
|
||||
jerk_max
|
||||
);
|
||||
|
||||
Self {
|
||||
mode: Mode::Fixed,
|
||||
enabled: false,
|
||||
multiplier: 1.0,
|
||||
speed: Velocity::ZERO,
|
||||
strategies: Strategies { fixed, adaptive },
|
||||
acceleration_controller,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, t: Instant)
|
||||
{
|
||||
use Mode::*;
|
||||
|
||||
let speed = match self.enabled
|
||||
{
|
||||
true => match self.mode
|
||||
{
|
||||
Fixed => self.strategies.fixed.compute(),
|
||||
Adaptive => self.strategies.adaptive.compute(),
|
||||
},
|
||||
false => Velocity::ZERO,
|
||||
};
|
||||
|
||||
// Apply acceleration control
|
||||
self.speed = self.acceleration_controller.update(speed * self.multiplier, t);
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> Mode
|
||||
{
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: Mode)
|
||||
{
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, value: bool)
|
||||
{
|
||||
self.enabled = value;
|
||||
}
|
||||
|
||||
pub fn speed(&self) -> Velocity
|
||||
{
|
||||
self.speed
|
||||
}
|
||||
|
||||
pub fn set_multiplier(&mut self, multiplier: f64)
|
||||
{
|
||||
self.multiplier = multiplier;
|
||||
}
|
||||
|
||||
pub fn strategies(&self) -> &Strategies
|
||||
{
|
||||
&self.strategies
|
||||
}
|
||||
|
||||
pub fn strategies_mut(&mut self) -> &mut Strategies
|
||||
{
|
||||
&mut self.strategies
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Strategies
|
||||
{
|
||||
pub fixed: FixedSpeedStrategy,
|
||||
pub adaptive: AdaptiveSpeedStrategy,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FixedSpeedStrategy
|
||||
{
|
||||
target_speed: Velocity,
|
||||
}
|
||||
|
||||
impl FixedSpeedStrategy
|
||||
{
|
||||
pub fn compute(&self) -> Velocity
|
||||
{
|
||||
self.target_speed
|
||||
}
|
||||
|
||||
pub fn target_speed(&self) -> Velocity {
|
||||
self.target_speed
|
||||
}
|
||||
|
||||
pub fn set_target_speed(&mut self, speed: Velocity) {
|
||||
self.target_speed = speed;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AdaptiveSpeedStrategy {
|
||||
base_speed: Velocity,
|
||||
deviation_max: Velocity,
|
||||
modulation: f64, // (-1.0 to 1.0)
|
||||
}
|
||||
|
||||
impl AdaptiveSpeedStrategy
|
||||
{
|
||||
pub fn compute(&self) -> Velocity
|
||||
{
|
||||
self.base_speed + (self.deviation_max * self.modulation)
|
||||
}
|
||||
|
||||
pub fn base_speed(&self) -> Velocity {
|
||||
self.base_speed
|
||||
}
|
||||
|
||||
pub fn set_base_speed(&mut self, speed: Velocity) {
|
||||
self.base_speed = speed;
|
||||
}
|
||||
|
||||
pub fn deviation_max(&self) -> Velocity {
|
||||
self.deviation_max
|
||||
}
|
||||
|
||||
pub fn set_deviation_max(&mut self, deviation: Velocity) {
|
||||
self.deviation_max = deviation;
|
||||
}
|
||||
|
||||
pub fn set_modulation(&mut self, modulation: f64) {
|
||||
self.modulation = modulation.clamp(-1.0, 1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use units::AngularVelocity;
|
||||
use control_core::converters::angular_step_converter::AngularStepConverter;
|
||||
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
|
||||
|
||||
use crate::types::Direction;
|
||||
use crate::winder2::devices::{ Puller, TensionArm };
|
||||
|
||||
use super::OperationState;
|
||||
|
||||
mod speed_controller;
|
||||
use speed_controller::{SpeedController, AdaptiveSpeedController, MinMaxSpeedController};
|
||||
|
||||
/// Represents the spool motor
|
||||
#[derive(Debug)]
|
||||
pub struct Spool
|
||||
{
|
||||
motor: StepperVelocityEL70x1,
|
||||
operation_state: OperationState,
|
||||
direction: Direction,
|
||||
speed_control_mode: SpeedControlMode,
|
||||
step_converter: AngularStepConverter,
|
||||
|
||||
pub speed_controllers: SpeedControllers,
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl Spool
|
||||
{
|
||||
pub fn new(motor: StepperVelocityEL70x1) -> Self
|
||||
{
|
||||
Self {
|
||||
motor,
|
||||
operation_state: OperationState::Disabled,
|
||||
direction: Direction::Forward,
|
||||
speed_controllers: SpeedControllers::new(),
|
||||
speed_control_mode: SpeedControlMode::Adaptive,
|
||||
step_converter: AngularStepConverter::new(200),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, t: Instant,tension_arm: &TensionArm, puller: &Puller)
|
||||
{
|
||||
let multiplier = if self.direction == Direction::Forward { 1.0 } else { -1.0 };
|
||||
|
||||
let controller = self.active_controller_mut();
|
||||
|
||||
let velocity = controller.update_speed(t, multiplier, tension_arm, puller);
|
||||
|
||||
let steps_per_second = self.step_converter.angular_velocity_to_steps(velocity);
|
||||
|
||||
_ = self.motor.set_speed(steps_per_second);
|
||||
}
|
||||
}
|
||||
|
||||
// getter + setter
|
||||
impl Spool
|
||||
{
|
||||
pub fn speed_control_mode(&self) -> SpeedControlMode
|
||||
{
|
||||
self.speed_control_mode
|
||||
}
|
||||
|
||||
pub fn set_speed_control_mode(&mut self, mode: SpeedControlMode)
|
||||
{
|
||||
if self.speed_control_mode == mode { return; }
|
||||
|
||||
self.speed_control_mode = mode;
|
||||
|
||||
// get active speed controller
|
||||
let controller = self.active_controller_mut();
|
||||
let current_speed = controller.speed();
|
||||
|
||||
// Set the speed in the target controller and reset it for smooth transition
|
||||
controller.set_speed(current_speed);
|
||||
controller.reset();
|
||||
controller.set_speed(current_speed);
|
||||
}
|
||||
|
||||
pub fn speed(&self) -> AngularVelocity
|
||||
{
|
||||
self.active_controller().speed()
|
||||
}
|
||||
|
||||
pub fn set_operation_state(&mut self, operation_state: OperationState)
|
||||
{
|
||||
use OperationState::*;
|
||||
|
||||
// No change, nothing to do
|
||||
if self.operation_state == operation_state { return; }
|
||||
|
||||
// Leaving disabled state, enable motor
|
||||
if self.operation_state == Disabled {
|
||||
self.motor.set_enabled(true);
|
||||
}
|
||||
|
||||
self.active_controller_mut().set_enabled(operation_state == Running);
|
||||
self.operation_state = operation_state;
|
||||
}
|
||||
|
||||
pub fn direction(&self) -> Direction
|
||||
{
|
||||
self.direction
|
||||
}
|
||||
|
||||
pub fn set_direction(&mut self, direction: Direction)
|
||||
{
|
||||
self.direction = direction;
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
impl Spool
|
||||
{
|
||||
fn active_controller(&self) -> &dyn SpeedController
|
||||
{
|
||||
use SpeedControlMode::*;
|
||||
|
||||
match self.speed_control_mode
|
||||
{
|
||||
Adaptive => &self.speed_controllers.adaptive,
|
||||
MinMax => &self.speed_controllers.minmax,
|
||||
}
|
||||
}
|
||||
|
||||
fn active_controller_mut(&mut self) -> &mut dyn SpeedController
|
||||
{
|
||||
use SpeedControlMode::*;
|
||||
|
||||
match self.speed_control_mode
|
||||
{
|
||||
Adaptive => &mut self.speed_controllers.adaptive,
|
||||
MinMax => &mut self.speed_controllers.minmax,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other types
|
||||
#[derive(Debug)]
|
||||
pub struct SpeedControllers
|
||||
{
|
||||
pub minmax: MinMaxSpeedController,
|
||||
pub adaptive: AdaptiveSpeedController,
|
||||
}
|
||||
|
||||
impl SpeedControllers
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
minmax: MinMaxSpeedController::new(),
|
||||
adaptive: AdaptiveSpeedController::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SpeedControlMode
|
||||
{
|
||||
Adaptive,
|
||||
MinMax,
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::AngularVelocity;
|
||||
|
||||
mod helpers;
|
||||
|
||||
mod min_max;
|
||||
pub use min_max::MinMaxSpeedController;
|
||||
|
||||
mod adapative;
|
||||
pub use adapative::AdaptiveSpeedController;
|
||||
|
||||
use crate::winder2::devices::{Puller, TensionArm};
|
||||
|
||||
pub trait SpeedController
|
||||
{
|
||||
fn speed(&self) -> AngularVelocity;
|
||||
fn set_speed(&mut self, speed: AngularVelocity);
|
||||
|
||||
fn is_enabled(&self) -> bool;
|
||||
fn set_enabled(&mut self, enabled: bool);
|
||||
|
||||
fn reset(&mut self);
|
||||
|
||||
fn update_speed(
|
||||
&mut self,
|
||||
t: Instant,
|
||||
multiplier: f64,
|
||||
tension_arm: &TensionArm,
|
||||
puller: &Puller
|
||||
) -> AngularVelocity;
|
||||
}
|
||||
@@ -1,649 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::converters::linear_step_converter::LinearStepConverter;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
|
||||
use units::AngularVelocity;
|
||||
use units::ConstZero;
|
||||
use units::Length;
|
||||
use units::Velocity;
|
||||
use units::angular_velocity::revolution_per_second;
|
||||
use units::length::millimeter;
|
||||
use units::velocity::millimeter_per_second;
|
||||
|
||||
use crate::winder2::devices::Spool;
|
||||
|
||||
use super::OperationState;
|
||||
|
||||
/// Represents the puller motor
|
||||
#[derive(Debug)]
|
||||
pub struct Traverse
|
||||
{
|
||||
// hardware
|
||||
motor: StepperVelocityEL70x1,
|
||||
laser_pointer: DigitalOutput,
|
||||
limit_switch: DigitalInput,
|
||||
|
||||
// config
|
||||
operation_state: OperationState,
|
||||
position: Length,
|
||||
limit_inner: Length,
|
||||
limit_outer: Length,
|
||||
step_size: Length,
|
||||
padding: Length,
|
||||
state: State,
|
||||
|
||||
// converters
|
||||
fullstep_converter: LinearStepConverter,
|
||||
microstep_converter: LinearStepConverter,
|
||||
}
|
||||
|
||||
// constants
|
||||
impl Traverse
|
||||
{
|
||||
const DEFAULT_LIMIT_INNER: f64 = 22.0; // in mm
|
||||
const DEFAULT_LIMIT_OUTER: f64 = 92.0; // in mm
|
||||
const DEFAULT_PADDING: f64 = 0.88; // in mm
|
||||
const DEFAULT_STEP_SIZE: f64 = 1.75; // in mm
|
||||
const CIRCUMFERENCE: f64 = 35.0; // in mm
|
||||
const STEPS_PER_REVOLUTION: i16 = 200;
|
||||
const MICRO_STEPS_COUNT: i16 = 64;
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl Traverse
|
||||
{
|
||||
pub fn new(
|
||||
motor: StepperVelocityEL70x1,
|
||||
limit_switch: DigitalInput,
|
||||
laser_pointer: DigitalOutput,
|
||||
) -> Self
|
||||
{
|
||||
let limit_inner = Length::new::<millimeter>(Self::DEFAULT_LIMIT_INNER);
|
||||
let limit_outer = Length::new::<millimeter>(Self::DEFAULT_LIMIT_OUTER);
|
||||
let circumference = Length::new::<millimeter>(Self::CIRCUMFERENCE);
|
||||
|
||||
let fullstep_converter = LinearStepConverter::from_circumference(
|
||||
Self::STEPS_PER_REVOLUTION,
|
||||
circumference,
|
||||
);
|
||||
|
||||
let microstep_converter = LinearStepConverter::from_circumference(
|
||||
Self::STEPS_PER_REVOLUTION * Self::MICRO_STEPS_COUNT,
|
||||
circumference,
|
||||
);
|
||||
|
||||
let step_size = Length::new::<millimeter>(Self::DEFAULT_STEP_SIZE);
|
||||
let padding = Length::new::<millimeter>(Self::DEFAULT_PADDING);
|
||||
|
||||
Self {
|
||||
operation_state: OperationState::Disabled,
|
||||
motor,
|
||||
laser_pointer,
|
||||
limit_switch,
|
||||
fullstep_converter,
|
||||
microstep_converter,
|
||||
limit_inner,
|
||||
limit_outer,
|
||||
step_size,
|
||||
padding,
|
||||
position: Length::ZERO,
|
||||
state: State::NotHomed,
|
||||
}
|
||||
}
|
||||
|
||||
/// update the traverse. Returns true if the state
|
||||
/// changed from this update
|
||||
pub fn update(&mut self, spool: &Spool)
|
||||
{
|
||||
if self.operation_state == OperationState::Disabled { return; }
|
||||
|
||||
self.update_position();
|
||||
self.update_state();
|
||||
|
||||
let steps_per_second = self.compute_output_steps(spool.speed());
|
||||
_ = self.motor.set_speed(steps_per_second);
|
||||
}
|
||||
}
|
||||
|
||||
// getters + setters
|
||||
impl Traverse
|
||||
{
|
||||
pub fn set_operation_state(&mut self, device_state: OperationState)
|
||||
{
|
||||
use OperationState::*;
|
||||
|
||||
// No change, nothing to do
|
||||
if self.operation_state == device_state { return; }
|
||||
|
||||
// Leaving standby, enable motor
|
||||
if self.operation_state == Disabled
|
||||
{
|
||||
self.motor.set_enabled(true);
|
||||
}
|
||||
|
||||
match device_state // guranteed state change
|
||||
{
|
||||
Disabled => self.motor.set_enabled(false),
|
||||
Holding => self.goto_home(),
|
||||
Running => self.start_traversing(),
|
||||
}
|
||||
|
||||
self.operation_state = device_state;
|
||||
}
|
||||
|
||||
pub fn state(&self) -> State
|
||||
{
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn limit_inner(&self) -> Length { self.limit_inner }
|
||||
|
||||
pub fn try_set_limit_inner(&mut self, limit_inner: Length) -> Result<(), ()>
|
||||
{
|
||||
// Validate the new inner limit against current outer limit
|
||||
if !Self::validate_traverse_limits(limit_inner, self.limit_outer)
|
||||
{
|
||||
// Don't update if validation fails - keep the current value
|
||||
return Err(());
|
||||
}
|
||||
|
||||
self.limit_inner = limit_inner;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn limit_outer(&self) -> Length { self.limit_outer }
|
||||
pub fn try_set_limit_outer(&mut self, limit_outer: Length) -> Result<(), ()>
|
||||
{
|
||||
// Validate the new outer limit against current inner limit
|
||||
if !Self::validate_traverse_limits(self.limit_inner, limit_outer)
|
||||
{
|
||||
// Don't update if validation fails - keep the current value
|
||||
return Err(());
|
||||
}
|
||||
|
||||
self.limit_outer = limit_outer;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn step_size(&self) -> Length { self.step_size }
|
||||
pub fn set_step_size(&mut self, step_size: Length) { self.step_size = step_size; }
|
||||
|
||||
pub fn padding(&self) -> Length { self.padding }
|
||||
pub fn set_padding(&mut self, padding: Length) { self.padding = padding; }
|
||||
|
||||
pub fn current_position(&self) -> Option<Length>
|
||||
{
|
||||
match self.is_homed()
|
||||
{
|
||||
true => Some(self.position),
|
||||
false => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn laser_pointer_enabled(&self) -> bool
|
||||
{
|
||||
self.laser_pointer.get()
|
||||
}
|
||||
|
||||
pub fn set_laser_pointer_enabled(&mut self, value: bool)
|
||||
{
|
||||
self.laser_pointer.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
// State management
|
||||
impl Traverse
|
||||
{
|
||||
pub fn can_goto_limit_inner(&self) -> bool
|
||||
{
|
||||
self.operation_state == OperationState::Disabled
|
||||
|| !self.is_homed()
|
||||
|| self.is_going_in()
|
||||
|| self.is_going_home()
|
||||
|| self.is_traversing()
|
||||
}
|
||||
|
||||
pub fn try_goto_limit_inner(&mut self) -> Result<(), ()>
|
||||
{
|
||||
if !self.can_goto_limit_inner()
|
||||
{
|
||||
return Err(());
|
||||
}
|
||||
|
||||
self.state = State::GoingIn;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn can_goto_limit_outer(&self)-> bool
|
||||
{
|
||||
self.operation_state == OperationState::Disabled
|
||||
|| !self.is_homed()
|
||||
|| self.is_going_out()
|
||||
|| self.is_going_home()
|
||||
|| self.is_traversing()
|
||||
}
|
||||
|
||||
pub fn try_goto_limit_outer(&mut self) -> Result<(), ()>
|
||||
{
|
||||
if !self.can_goto_limit_outer()
|
||||
{
|
||||
return Err(());
|
||||
}
|
||||
|
||||
self.state = State::GoingOut;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn can_go_home(&self)-> bool
|
||||
{
|
||||
self.operation_state == OperationState::Disabled
|
||||
|| !self.is_homed()
|
||||
|| self.is_going_out()
|
||||
|| self.is_going_home()
|
||||
|| self.is_traversing()
|
||||
}
|
||||
|
||||
pub fn try_goto_home(&mut self) -> Result<(), ()>
|
||||
{
|
||||
if !self.can_go_home()
|
||||
{
|
||||
return Err(());
|
||||
}
|
||||
|
||||
self.goto_home();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn goto_home(&mut self)
|
||||
{
|
||||
self.state = State::Homing(HomingState::Initialize);
|
||||
}
|
||||
|
||||
fn start_traversing(&mut self)
|
||||
{
|
||||
self.state = State::Traversing(TraversingState::GoingOut);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_idle(&self) -> bool
|
||||
{
|
||||
self.state == State::Idle
|
||||
}
|
||||
|
||||
pub fn is_homed(&self) -> bool {
|
||||
// if not [`State::NotHomed`], then it is homed
|
||||
!matches!(self.state, State::NotHomed)
|
||||
}
|
||||
|
||||
pub fn is_going_in(&self) -> bool {
|
||||
self.state == State::GoingIn
|
||||
}
|
||||
|
||||
pub fn is_going_out(&self) -> bool {
|
||||
self.state == State::GoingOut
|
||||
}
|
||||
|
||||
pub fn is_going_home(&self) -> bool {
|
||||
matches!(self.state, State::Homing(_))
|
||||
}
|
||||
|
||||
pub fn is_traversing(&self) -> bool {
|
||||
matches!(self.state, State::Traversing(_))
|
||||
}
|
||||
}
|
||||
|
||||
// helpers
|
||||
impl Traverse
|
||||
{
|
||||
fn compute_output_steps(&self, spool_speed: AngularVelocity) -> f64
|
||||
{
|
||||
let speed = self.veloctity_from_state(spool_speed);
|
||||
self.fullstep_converter.velocity_to_steps(speed)
|
||||
}
|
||||
|
||||
pub fn update_position(&mut self)
|
||||
{
|
||||
let steps = self.motor.get_position() as f64;
|
||||
self.position = self.microstep_converter.steps_to_distance(steps);
|
||||
}
|
||||
|
||||
fn endstop_triggered(&self) -> bool
|
||||
{
|
||||
self.limit_switch.get_value().unwrap_or(false)
|
||||
}
|
||||
|
||||
fn calculate_traverse_speed(spool_speed: AngularVelocity, step_size: Length) -> Velocity
|
||||
{
|
||||
let spool_speed = spool_speed.get::<revolution_per_second>();
|
||||
let step_size = step_size.get::<millimeter>();
|
||||
|
||||
// Calculate the traverse speed directly from spool speed and step size
|
||||
Velocity::new::<millimeter_per_second>(spool_speed * step_size)
|
||||
}
|
||||
|
||||
// Changes the direction of the speed based on the current position and target position
|
||||
fn speed_to_position(&self, target_position: Length, absolute_speed: Velocity) -> Velocity
|
||||
{
|
||||
// If we are over the target position we need to move negative
|
||||
if self.position > target_position {
|
||||
-absolute_speed.abs()
|
||||
} else if self.position < target_position {
|
||||
absolute_speed.abs()
|
||||
} else {
|
||||
Velocity::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate distance to position
|
||||
fn distance_to_position(&self, target_position: Length) -> Length
|
||||
{
|
||||
(self.position - target_position).abs()
|
||||
}
|
||||
|
||||
// If at inner limit within a tolerance
|
||||
fn is_at_position(&self, target_position: Length, tolerance: Length) -> bool
|
||||
{
|
||||
let upper_tolerance = target_position + tolerance.abs();
|
||||
let lower_tolerance = target_position - tolerance.abs();
|
||||
lower_tolerance <= self.position && self.position <= upper_tolerance
|
||||
}
|
||||
|
||||
/// Validates that traverse limits maintain proper constraints:
|
||||
/// - Inner limit must be smaller than outer limit
|
||||
/// - At least 0.9mm difference between inner and outer limits
|
||||
fn validate_traverse_limits(inner: Length, outer: Length) -> bool
|
||||
{
|
||||
outer > inner + Length::new::<millimeter>(0.9)
|
||||
}
|
||||
}
|
||||
|
||||
// state updates
|
||||
impl Traverse
|
||||
{
|
||||
fn update_state(&mut self)
|
||||
{
|
||||
use State::*;
|
||||
|
||||
match self.state
|
||||
{
|
||||
NotHomed | Idle => {}
|
||||
GoingIn =>
|
||||
{
|
||||
// If inner limit is reached
|
||||
if self.is_at_position(self.limit_inner, Length::new::<millimeter>(0.01))
|
||||
{
|
||||
// Put Into Idle
|
||||
self.state = State::Idle;
|
||||
}
|
||||
}
|
||||
GoingOut =>
|
||||
{
|
||||
// If outer limit is reached
|
||||
if self.is_at_position(self.limit_outer, Length::new::<millimeter>(0.01)) {
|
||||
// Put Into Idle
|
||||
self.state = State::Idle;
|
||||
}
|
||||
}
|
||||
Homing(state) => self.update_homing_state(state),
|
||||
Traversing(state) => self.update_traversing_state(state),
|
||||
};
|
||||
}
|
||||
|
||||
fn update_homing_state(&mut self, homing_state: HomingState)
|
||||
{
|
||||
use HomingState::*;
|
||||
|
||||
match homing_state
|
||||
{
|
||||
Initialize =>
|
||||
{
|
||||
// If endstop is triggered, escape the endstop
|
||||
if self.endstop_triggered() {
|
||||
self.state = State::Homing(EscapeEndstop);
|
||||
} else {
|
||||
// If endstop is not triggered, move to the endstop
|
||||
self.state = State::Homing(FindEndstopCoarse);
|
||||
}
|
||||
}
|
||||
EscapeEndstop => {
|
||||
// Move out until endstop is not triggered anymore
|
||||
if !self.endstop_triggered() {
|
||||
self.state = State::Homing(FindEndstopFineDistancing);
|
||||
}
|
||||
}
|
||||
FindEndstopFineDistancing => {
|
||||
// Move out until endstop is not triggered anymore
|
||||
if !self.endstop_triggered() {
|
||||
// Find endstop fine
|
||||
self.state = State::Homing(FindEndtopFine);
|
||||
}
|
||||
}
|
||||
FindEndtopFine => {
|
||||
// If endstop is reached change to idle
|
||||
if self.endstop_triggered() {
|
||||
// Set poition of traverse to 0
|
||||
self.motor.set_position(0);
|
||||
// Put Into Idle
|
||||
self.state = State::Homing(Validate(Instant::now()));
|
||||
}
|
||||
}
|
||||
FindEndstopCoarse => {
|
||||
// Move to endstop
|
||||
if self.endstop_triggered() {
|
||||
// Move awaiy from endstop
|
||||
self.state = State::Homing(FindEndstopFineDistancing);
|
||||
}
|
||||
}
|
||||
Validate(instant) => {
|
||||
// If 100ms have passed check if position is actually 0.0
|
||||
if instant.elapsed().as_millis() > 100 {
|
||||
if self.is_at_position(Length::ZERO, Length::new::<millimeter>(0.01)) {
|
||||
// If position is 0.0, put into idle
|
||||
self.state = State::Idle;
|
||||
} else {
|
||||
// If position is not 0.0, redo homing
|
||||
self.state = State::Homing(Initialize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_traversing_state(&mut self, state: TraversingState)
|
||||
{
|
||||
use TraversingState::*;
|
||||
|
||||
match state {
|
||||
GoingOut => {
|
||||
// If outer limit is reached
|
||||
if self.position >= self.limit_outer - self.padding {
|
||||
// Turn around
|
||||
self.state = State::Traversing(TraversingIn);
|
||||
}
|
||||
}
|
||||
TraversingIn => {
|
||||
// If inner limit is reached
|
||||
if self.position <= self.limit_inner + self.padding {
|
||||
// Turn around
|
||||
self.state = State::Traversing(TraversingOut);
|
||||
}
|
||||
}
|
||||
TraversingOut => {
|
||||
// If outer limit is reached
|
||||
if self.position >= self.limit_outer - self.padding {
|
||||
// Turn around
|
||||
self.state = State::Traversing(TraversingIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// velocity computation
|
||||
impl Traverse
|
||||
{
|
||||
fn veloctity_from_state(&self, spool_speed: AngularVelocity) -> Velocity
|
||||
{
|
||||
use State::*;
|
||||
|
||||
match self.state {
|
||||
NotHomed => Velocity::ZERO, // Not homed, no movement
|
||||
Idle => Velocity::ZERO, // No movement in idle state
|
||||
GoingIn => {
|
||||
// Move in at a speed of 10-100 mm/s
|
||||
self.speed_to_position(
|
||||
self.limit_inner,
|
||||
match self.distance_to_position(self.limit_inner).abs()
|
||||
> Length::new::<millimeter>(1.0)
|
||||
{
|
||||
true => Velocity::new::<millimeter_per_second>(100.0),
|
||||
false => Velocity::new::<millimeter_per_second>(10.0),
|
||||
},
|
||||
)
|
||||
},
|
||||
GoingOut => {
|
||||
// Move out at a speed of 10-100 mm/s
|
||||
self.speed_to_position(
|
||||
self.limit_outer,
|
||||
match self.distance_to_position(self.limit_outer).abs()
|
||||
> Length::new::<millimeter>(1.0)
|
||||
{
|
||||
true => Velocity::new::<millimeter_per_second>(100.0),
|
||||
false => Velocity::new::<millimeter_per_second>(10.0),
|
||||
},
|
||||
)
|
||||
}
|
||||
Homing(state) => Self::velocity_from_homing_state(state),
|
||||
Traversing(state) => self.velocity_from_traversing_state(state, spool_speed),
|
||||
}
|
||||
}
|
||||
|
||||
fn velocity_from_homing_state(homing_state: HomingState) -> Velocity
|
||||
{
|
||||
use units::velocity::millimeter_per_second as mmps;
|
||||
use HomingState::*;
|
||||
|
||||
match homing_state {
|
||||
Initialize => Velocity::ZERO,
|
||||
// Move out at a speed of 10 mm/s
|
||||
EscapeEndstop => Velocity::new::<mmps>(10.0),
|
||||
// Move out at a speed of 2 mm/s
|
||||
FindEndstopFineDistancing => Velocity::new::<mmps>(2.0),
|
||||
// Move in at a speed of -100 mm/s
|
||||
FindEndstopCoarse => Velocity::new::<mmps>(-100.0),
|
||||
// move into the endstop at 2 mm/s
|
||||
FindEndtopFine => Velocity::new::<mmps>(-2.0),
|
||||
// We stand still until the validation cooldown has passed
|
||||
Validate(_) => Velocity::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
fn velocity_from_traversing_state(
|
||||
&self,
|
||||
traversing_state: TraversingState,
|
||||
spool_speed: AngularVelocity
|
||||
) -> Velocity
|
||||
{
|
||||
use TraversingState::*;
|
||||
|
||||
match traversing_state {
|
||||
GoingOut => {
|
||||
// Move out at a speed of 100 mm/s
|
||||
self.speed_to_position(
|
||||
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
|
||||
Velocity::new::<millimeter_per_second>(100.0),
|
||||
)
|
||||
}
|
||||
TraversingIn =>
|
||||
{
|
||||
let offset = Length::new::<millimeter>(0.01);
|
||||
let target_position = self.limit_inner + self.padding - offset;
|
||||
let absolute_speed = Self::calculate_traverse_speed(spool_speed, self.step_size);
|
||||
self.speed_to_position(target_position, absolute_speed)
|
||||
}
|
||||
TraversingOut =>
|
||||
{
|
||||
let offset = Length::new::<millimeter>(0.01);
|
||||
let target_position = self.limit_outer - self.padding + offset;
|
||||
let absolute_speed = Self::calculate_traverse_speed(spool_speed, self.step_size);
|
||||
self.speed_to_position(target_position, absolute_speed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other types
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum State
|
||||
{
|
||||
/// Initial state
|
||||
NotHomed,
|
||||
|
||||
/// Doing nothing
|
||||
/// Already homed
|
||||
Idle,
|
||||
|
||||
/// Going to inner limit
|
||||
///
|
||||
/// After reaching the inner limit, the state will change to [`State::Idle`]
|
||||
GoingIn,
|
||||
|
||||
/// Going to outer limit
|
||||
///
|
||||
/// After reaching the outer limit, the state will change to [`State::Idle`]
|
||||
GoingOut,
|
||||
|
||||
/// Homing is in progress
|
||||
///
|
||||
/// After homing is done, the state will change to [`State::Idle`]
|
||||
Homing(HomingState),
|
||||
|
||||
/// Move between inner and outer limits
|
||||
Traversing(TraversingState),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum TraversingState
|
||||
{
|
||||
/// Like [`State::GoingOut`] but
|
||||
/// - will go into [`State::GoingIn`] after reaching the outer limit
|
||||
GoingOut,
|
||||
|
||||
/// Like [`State::GoingIn`] but
|
||||
/// - will go into [`State::GoingOut`] after reaching the inner limit
|
||||
/// - speed is synced to spool speed
|
||||
TraversingIn,
|
||||
|
||||
/// Like [`State::GoingOut`] but
|
||||
/// - will go into [`State::GoingIn`] after reaching the outer limit
|
||||
/// - speed is synced to spool speed
|
||||
TraversingOut,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum HomingState
|
||||
{
|
||||
/// In this state the traverse is not moving but checks if the endstop si triggered
|
||||
/// If the endstop is triggered we go into [`HomingState::EscapeEndstop`]
|
||||
/// If the endstop is not triggered we go into [`HomingState::FindEndstop`]
|
||||
Initialize,
|
||||
|
||||
/// In this state the traverse is moving out away from the endstop until it's not triggered anymore
|
||||
/// The it goes into [`HomingState::FindEnstopFineDistancing`]
|
||||
EscapeEndstop,
|
||||
|
||||
/// Moving out away from the endstop
|
||||
/// Then Transition into [`HomingState::FindEndtopFine`]
|
||||
FindEndstopFineDistancing,
|
||||
|
||||
/// In this state the traverse is fast until it reaches the endstop
|
||||
FindEndstopCoarse,
|
||||
|
||||
/// In this state the traverse is moving slowly until it reaches the endstop
|
||||
FindEndtopFine,
|
||||
|
||||
/// In this state we check if th current position is actually 0.0, if not we redo the homing routine
|
||||
Validate(Instant),
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
mod winder2_imports {
|
||||
pub use super::super::puller_speed_controller::PullerRegulationMode;
|
||||
pub use super::super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
|
||||
pub use crate::buffer1::BufferV1;
|
||||
pub use api::{
|
||||
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
|
||||
SpoolAutomaticActionState, SpoolSpeedControllerState, StateEvent, TensionArmState,
|
||||
TraverseState, Winder2Events,
|
||||
};
|
||||
pub use control_core::socketio::event::BuildEvent;
|
||||
pub use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
pub use std::time::Instant;
|
||||
pub use units::{
|
||||
angle::degree,
|
||||
angular_velocity::revolution_per_minute,
|
||||
f64::*,
|
||||
length::{meter, millimeter},
|
||||
velocity::meter_per_minute,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub use winder2_imports::*;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl Winder2 {
|
||||
/// Implement Spool
|
||||
/// called by `act`
|
||||
pub fn sync_spool_speed(&mut self, t: Instant) {
|
||||
let angular_velocity = self.spool_speed_controller.update_speed(
|
||||
t,
|
||||
&self.tension_arm,
|
||||
&self.puller_speed_controller,
|
||||
);
|
||||
|
||||
// Apply direction based on forward setting
|
||||
let directed_angular_velocity = if self.spool_speed_controller.get_forward() {
|
||||
angular_velocity
|
||||
} else {
|
||||
-angular_velocity
|
||||
};
|
||||
|
||||
let steps_per_second = self
|
||||
.spool_step_converter
|
||||
.angular_velocity_to_steps(directed_angular_velocity);
|
||||
let _ = self.spool.set_speed(steps_per_second);
|
||||
}
|
||||
|
||||
pub fn stop_or_pull_spool(&mut self, now: Instant) {
|
||||
if matches!(
|
||||
self.spool_automatic_action.mode,
|
||||
SpoolAutomaticActionMode::NoAction
|
||||
) {
|
||||
self.calculate_spool_auto_progress_(now);
|
||||
return;
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
Winder2Mode::Pull => self.calculate_spool_auto_progress_(now),
|
||||
Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
|
||||
_ => {
|
||||
self.spool_automatic_action.progress_last_check = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
|
||||
match self.spool_automatic_action.mode {
|
||||
SpoolAutomaticActionMode::NoAction => (),
|
||||
SpoolAutomaticActionMode::Pull => {
|
||||
self.stop_or_pull_spool_reset(now);
|
||||
self.set_mode(&Winder2Mode::Pull);
|
||||
}
|
||||
SpoolAutomaticActionMode::Hold => {
|
||||
self.stop_or_pull_spool_reset(now);
|
||||
self.set_mode(&Winder2Mode::Hold);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Implement Mode
|
||||
pub fn set_mode(&mut self, mode: &Winder2Mode) {
|
||||
let should_update = *mode != Winder2Mode::Wind || self.can_wind();
|
||||
|
||||
if should_update {
|
||||
// all transitions are allowed
|
||||
self.mode = mode.clone();
|
||||
|
||||
// Apply the mode changes to the spool and puller
|
||||
self.set_spool_mode(mode);
|
||||
self.set_puller_mode(mode);
|
||||
self.set_traverse_mode(mode);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Implement Traverse
|
||||
pub fn set_laser(&mut self, value: bool) {
|
||||
self.laser.set(value);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
|
||||
let new_inner = Length::new::<millimeter>(limit);
|
||||
let current_outer = self.traverse_controller.get_limit_outer();
|
||||
|
||||
// Validate the new inner limit against current outer limit
|
||||
if !Self::validate_traverse_limits(new_inner, current_outer) {
|
||||
// Don't update if validation fails - keep the current value
|
||||
return;
|
||||
}
|
||||
self.traverse_controller.set_limit_inner(new_inner);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
|
||||
let new_outer = Length::new::<millimeter>(limit);
|
||||
let current_inner = self.traverse_controller.get_limit_inner();
|
||||
|
||||
// Validate the new outer limit against current inner limit
|
||||
if !Self::validate_traverse_limits(current_inner, new_outer) {
|
||||
// Don't update if validation fails - keep the current value
|
||||
return;
|
||||
}
|
||||
|
||||
self.traverse_controller.set_limit_outer(new_outer);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_step_size(&mut self, step_size: f64) {
|
||||
let step_size = Length::new::<millimeter>(step_size);
|
||||
self.traverse_controller.set_step_size(step_size);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_padding(&mut self, padding: f64) {
|
||||
let padding = Length::new::<millimeter>(padding);
|
||||
self.traverse_controller.set_padding(padding);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_inner(&mut self) {
|
||||
if self.can_go_in() {
|
||||
self.traverse_controller.goto_limit_inner();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_outer(&mut self) {
|
||||
if self.can_go_out() {
|
||||
self.traverse_controller.goto_limit_outer();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_home(&mut self) {
|
||||
if self.can_go_home() {
|
||||
self.traverse_controller.goto_home();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn get_live_values(&self) -> LiveValuesEvent {
|
||||
let angle_deg = self.tension_arm.get_angle().get::<degree>();
|
||||
|
||||
// Wrap [270;<360] to [-90; 0]
|
||||
// This is done to reduce flicker in the graphs around the zero point
|
||||
let angle_deg = if angle_deg >= 270.0 {
|
||||
angle_deg - 360.0
|
||||
} else {
|
||||
angle_deg
|
||||
};
|
||||
|
||||
// Calculate puller speed from current motor steps
|
||||
let steps_per_second = self.puller.get_speed();
|
||||
let angular_velocity = self
|
||||
.puller_speed_controller
|
||||
.converter
|
||||
.steps_to_angular_velocity(steps_per_second as f64);
|
||||
let motor_speed = self
|
||||
.puller_speed_controller
|
||||
.angular_velocity_to_speed(angular_velocity);
|
||||
|
||||
// Divide by gear ratio to get actual puller/material speed
|
||||
let puller_speed = motor_speed / self.puller_speed_controller.get_gear_ratio().multiplier();
|
||||
|
||||
// Calculate spool RPM from current motor steps (always positive regardless of direction)
|
||||
let spool_rpm = self
|
||||
.spool_step_converter
|
||||
.steps_to_angular_velocity(self.spool.get_speed() as f64)
|
||||
.get::<revolution_per_minute>()
|
||||
.abs();
|
||||
|
||||
LiveValuesEvent {
|
||||
traverse_position: self
|
||||
.traverse_controller
|
||||
.get_current_position()
|
||||
.map(|x| x.get::<millimeter>()),
|
||||
puller_speed: puller_speed.get::<meter_per_minute>().abs(),
|
||||
spool_rpm,
|
||||
tension_arm_angle: angle_deg,
|
||||
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let event = self.get_live_values().build();
|
||||
self.namespace.emit(Winder2Events::LiveValues(event));
|
||||
}
|
||||
|
||||
pub fn build_state_event(&mut self) -> StateEvent {
|
||||
use crate::MachineCrossConnectionState;
|
||||
|
||||
let connected_machine = self.connected_machines.get(0);
|
||||
let ident = match connected_machine {
|
||||
Some(machine) => Some(machine.ident.clone()),
|
||||
None => None,
|
||||
};
|
||||
let cross_conn = MachineCrossConnectionState {
|
||||
machine_identification_unique: ident,
|
||||
is_available: connected_machine.is_some(),
|
||||
};
|
||||
|
||||
StateEvent {
|
||||
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
|
||||
traverse_state: TraverseState {
|
||||
limit_inner: self
|
||||
.traverse_controller
|
||||
.get_limit_inner()
|
||||
.get::<millimeter>(),
|
||||
limit_outer: self
|
||||
.traverse_controller
|
||||
.get_limit_outer()
|
||||
.get::<millimeter>(),
|
||||
position_in: self
|
||||
.traverse_controller
|
||||
.get_limit_inner()
|
||||
.get::<millimeter>(),
|
||||
position_out: self
|
||||
.traverse_controller
|
||||
.get_limit_outer()
|
||||
.get::<millimeter>(),
|
||||
is_going_in: self.traverse_controller.is_going_in(),
|
||||
is_going_out: self.traverse_controller.is_going_out(),
|
||||
is_homed: self.traverse_controller.is_homed(),
|
||||
is_going_home: self.traverse_controller.is_going_home(),
|
||||
is_traversing: self.traverse_controller.is_traversing(),
|
||||
laserpointer: self.laser.get(),
|
||||
step_size: self.traverse_controller.get_step_size().get::<millimeter>(),
|
||||
padding: self.traverse_controller.get_padding().get::<millimeter>(),
|
||||
can_go_in: self.can_go_in(),
|
||||
can_go_out: self.can_go_out(),
|
||||
can_go_home: self.can_go_home(),
|
||||
},
|
||||
puller_state: PullerState {
|
||||
regulation: self.puller_speed_controller.regulation_mode.clone(),
|
||||
target_speed: self
|
||||
.puller_speed_controller
|
||||
.target_speed
|
||||
.get::<meter_per_minute>(),
|
||||
target_diameter: self
|
||||
.puller_speed_controller
|
||||
.target_diameter
|
||||
.get::<millimeter>(),
|
||||
forward: self.puller_speed_controller.forward,
|
||||
gear_ratio: self.puller_speed_controller.gear_ratio,
|
||||
},
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone().into(),
|
||||
can_wind: self.can_wind(),
|
||||
},
|
||||
tension_arm_state: TensionArmState {
|
||||
zeroed: self.tension_arm.zeroed,
|
||||
},
|
||||
spool_speed_controller_state: SpoolSpeedControllerState {
|
||||
regulation_mode: self.spool_speed_controller.get_type().clone(),
|
||||
minmax_min_speed: self
|
||||
.spool_speed_controller
|
||||
.get_minmax_min_speed()
|
||||
.get::<revolution_per_minute>(),
|
||||
minmax_max_speed: self
|
||||
.spool_speed_controller
|
||||
.get_minmax_max_speed()
|
||||
.get::<revolution_per_minute>(),
|
||||
adaptive_tension_target: self.spool_speed_controller.get_adaptive_tension_target(),
|
||||
adaptive_radius_learning_rate: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_radius_learning_rate(),
|
||||
adaptive_max_speed_multiplier: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_max_speed_multiplier(),
|
||||
adaptive_acceleration_factor: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_acceleration_factor(),
|
||||
adaptive_deacceleration_urgency_multiplier: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_deacceleration_urgency_multiplier(),
|
||||
forward: self.spool_speed_controller.get_forward(),
|
||||
},
|
||||
spool_automatic_action_state: SpoolAutomaticActionState {
|
||||
spool_required_meters: self.spool_automatic_action.target_length.get::<meter>(),
|
||||
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
|
||||
},
|
||||
connected_machine_state: cross_conn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self) {
|
||||
let state_event = self.build_state_event();
|
||||
let event = state_event.build();
|
||||
self.namespace.emit(Winder2Events::State(event));
|
||||
}
|
||||
|
||||
/// Apply the mode changes to the spool
|
||||
///
|
||||
/// It contains a transition matrix for atomic changes.
|
||||
/// It will set [`Self::spool_mode`]
|
||||
fn set_traverse_mode(&mut self, mode: &Winder2Mode) {
|
||||
// Convert to `Winder2Mode` to `TraverseMode`
|
||||
let mode: TraverseMode = mode.clone().into();
|
||||
|
||||
// If coming out of standby
|
||||
if self.traverse_mode == TraverseMode::Standby && mode != TraverseMode::Standby {
|
||||
self.traverse.set_enabled(true);
|
||||
self.traverse_controller.set_enabled(true);
|
||||
}
|
||||
|
||||
// If going into standby
|
||||
if mode == TraverseMode::Standby && self.traverse_mode != TraverseMode::Standby {
|
||||
// If we are going into standby, we need to stop the traverse
|
||||
self.traverse.set_enabled(false);
|
||||
self.traverse_controller.set_enabled(false);
|
||||
}
|
||||
|
||||
// Transition matrix
|
||||
match self.traverse_mode {
|
||||
TraverseMode::Standby => match mode {
|
||||
TraverseMode::Standby => {}
|
||||
TraverseMode::Hold => {
|
||||
// From [`TraverseMode::Standby`] to [`TraverseMode::Hold`]
|
||||
self.traverse.set_enabled(true);
|
||||
self.traverse_controller.set_enabled(true);
|
||||
self.traverse_controller.goto_home();
|
||||
}
|
||||
TraverseMode::Traverse => {
|
||||
// From [`TraverseMode::Standby`] to [`TraverseMode::Wind`]
|
||||
self.traverse.set_enabled(true);
|
||||
self.traverse_controller.set_enabled(true);
|
||||
self.traverse_controller.start_traversing();
|
||||
}
|
||||
},
|
||||
TraverseMode::Hold => match mode {
|
||||
TraverseMode::Standby => {
|
||||
// From [`TraverseMode::Hold`] to [`TraverseMode::Standby`]
|
||||
self.traverse.set_enabled(false);
|
||||
self.traverse_controller.set_enabled(false);
|
||||
}
|
||||
TraverseMode::Hold => {}
|
||||
TraverseMode::Traverse => {
|
||||
// From [`TraverseMode::Hold`] to [`TraverseMode::Wind`]
|
||||
self.traverse_controller.start_traversing();
|
||||
}
|
||||
},
|
||||
TraverseMode::Traverse => match mode {
|
||||
TraverseMode::Standby => {
|
||||
// From [`TraverseMode::Wind`] to [`TraverseMode::Standby`]
|
||||
self.traverse.set_enabled(false);
|
||||
self.traverse_controller.set_enabled(false);
|
||||
}
|
||||
TraverseMode::Hold => {
|
||||
// From [`TraverseMode::Wind`] to [`TraverseMode::Hold`]
|
||||
self.traverse_controller.goto_home();
|
||||
}
|
||||
TraverseMode::Traverse => {}
|
||||
},
|
||||
}
|
||||
|
||||
// Update the internal state
|
||||
self.traverse_mode = mode;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Implement Tension Arm
|
||||
pub fn tension_arm_zero(&mut self) {
|
||||
self.tension_arm.zero();
|
||||
self.emit_live_values(); // For angle update
|
||||
// For state update
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
|
||||
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
|
||||
self.spool_automatic_action.mode = mode;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
|
||||
self.puller_speed_controller
|
||||
.set_regulation_mode(puller_regulation_mode);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set target speed in m/min
|
||||
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
|
||||
// Convert m/min to velocity
|
||||
let target_speed = Velocity::new::<meter_per_minute>(target_speed);
|
||||
self.puller_speed_controller.set_target_speed(target_speed);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set target diameter in mm
|
||||
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
|
||||
// Convert m/min to velocity
|
||||
let target_diameter = Length::new::<millimeter>(target_diameter);
|
||||
self.puller_speed_controller
|
||||
.set_target_diameter(target_diameter);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set forward direction
|
||||
pub fn puller_set_forward(&mut self, forward: bool) {
|
||||
self.puller_speed_controller.set_forward(forward);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set gear ratio for winding speed
|
||||
pub fn puller_set_gear_ratio(&mut self, gear_ratio: super::puller_speed_controller::GearRatio) {
|
||||
self.puller_speed_controller.set_gear_ratio(gear_ratio);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
// Spool Speed Controller API methods
|
||||
pub fn spool_set_regulation_mode(
|
||||
&mut self,
|
||||
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
|
||||
) {
|
||||
self.spool_speed_controller.set_type(regulation_mode);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set minimum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
|
||||
let min_speed = AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
|
||||
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
|
||||
tracing::error!("Failed to set spool min speed: {:?}", e);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set maximum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
|
||||
let max_speed = AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
|
||||
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
|
||||
tracing::error!("Failed to set spool max speed: {:?}", e);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set tension target for adaptive mode (0.0-1.0)
|
||||
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_tension_target(tension_target);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set radius learning rate for adaptive mode
|
||||
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_radius_learning_rate(radius_learning_rate);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set max speed multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_max_speed_multiplier(max_speed_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set acceleration factor for adaptive mode
|
||||
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_acceleration_factor(acceleration_factor);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set deacceleration urgency multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set forward rotation direction
|
||||
pub fn spool_set_forward(&mut self, forward: bool) {
|
||||
self.spool_speed_controller.set_forward(forward);
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+127
-99
@@ -1,15 +1,9 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{
|
||||
ConstZero,
|
||||
f64::*,
|
||||
angle::degree,
|
||||
angular_acceleration::{radian_per_second_squared, revolution_per_minute_per_second},
|
||||
angular_velocity::{radian_per_second, revolution_per_minute, revolution_per_second},
|
||||
length::meter,
|
||||
velocity::meter_per_second,
|
||||
use crate::winder2::{
|
||||
clamp_revolution::clamp_revolution_uom, filament_tension::FilamentTensionCalculator,
|
||||
puller_speed_controller::PullerSpeedController,
|
||||
};
|
||||
|
||||
use super::{clamp_revolution::Clamping, tension_arm::TensionArm};
|
||||
use control_core::{
|
||||
controllers::{
|
||||
first_degree_motion::angular_acceleration_speed_controller::AngularAccelerationSpeedController,
|
||||
@@ -20,16 +14,20 @@ use control_core::{
|
||||
moving_time_window::MovingTimeWindow,
|
||||
},
|
||||
};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{helpers::{Clamp, clamp_revolution_uom}, winder2::devices::{Puller, spool::speed_controller::SpeedController}};
|
||||
use crate::winder2::devices::TensionArm;
|
||||
use super::helpers::FilamentTensionCalculator;
|
||||
use units::ConstZero;
|
||||
use units::angle::degree;
|
||||
use units::angular_acceleration::{radian_per_second_squared, revolution_per_minute_per_second};
|
||||
use units::angular_velocity::{radian_per_second, revolution_per_minute, revolution_per_second};
|
||||
use units::f64::*;
|
||||
use units::length::meter;
|
||||
use units::velocity::meter_per_second;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MinMaxSpeedController
|
||||
{
|
||||
pub struct MinMaxSpoolSpeedController {
|
||||
/// Current speed in
|
||||
speed: AngularVelocity,
|
||||
last_speed: AngularVelocity,
|
||||
/// Whether the speed controller is enabled or not
|
||||
enabled: bool,
|
||||
/// Acceleration controller to dampen speed change
|
||||
@@ -40,14 +38,23 @@ pub struct MinMaxSpeedController
|
||||
speed_time_window: MovingTimeWindow<f64>,
|
||||
}
|
||||
|
||||
impl MinMaxSpeedController
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
impl Default for MinMaxSpoolSpeedController {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MinMaxSpoolSpeedController {
|
||||
/// Parameters:
|
||||
/// - `min_speed`: Minimum speed
|
||||
/// - `max_speed`: Maximum speed
|
||||
/// - `acceleration`: Acceleration
|
||||
/// - `deceleration`: Deceleration (preferably negative)
|
||||
pub fn new() -> Self {
|
||||
let max_speed = AngularVelocity::new::<revolution_per_minute>(150.0);
|
||||
|
||||
Self {
|
||||
speed: AngularVelocity::ZERO,
|
||||
last_speed: AngularVelocity::ZERO,
|
||||
enabled: false,
|
||||
acceleration_controller: AngularAccelerationSpeedController::new(
|
||||
Some(AngularVelocity::ZERO),
|
||||
@@ -68,13 +75,44 @@ impl MinMaxSpeedController
|
||||
}
|
||||
}
|
||||
|
||||
impl MinMaxSpeedController
|
||||
{
|
||||
fn speed_raw(&mut self, _t: Instant, tension_arm_angle: Angle) -> AngularVelocity
|
||||
{
|
||||
impl MinMaxSpoolSpeedController {
|
||||
/// Helper method to get min speed without Option type
|
||||
fn min_speed(&self) -> AngularVelocity {
|
||||
self.acceleration_controller
|
||||
.get_min_speed()
|
||||
.unwrap_or(AngularVelocity::ZERO)
|
||||
}
|
||||
|
||||
/// Helper method to get max speed without Option type
|
||||
fn max_speed(&self) -> AngularVelocity {
|
||||
self.acceleration_controller
|
||||
.get_max_speed()
|
||||
.unwrap_or(AngularVelocity::new::<radian_per_second>(f64::INFINITY))
|
||||
}
|
||||
|
||||
/// Calculates the desired speed based on the tension arm angle.
|
||||
///
|
||||
/// If the arm is over it's maximum angle, the speed is set to the minimum speed.
|
||||
/// If the arm is under it's minimum angle, the speed is set to the maximum speed.
|
||||
/// If the arm is within the range, the speed is interpolated between the minimum and maximum speed based on the tension arm angle.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `t`: The current time.
|
||||
/// - `tension_arm`: A reference to the `TensionArm` instance that provides the angle of the tension arm.
|
||||
///
|
||||
/// Returns:
|
||||
/// - speed
|
||||
fn speed_raw(&mut self, _t: Instant, tension_arm: &TensionArm) -> AngularVelocity {
|
||||
let min_speed = AngularVelocity::ZERO;
|
||||
|
||||
// Convert puller speed to angular velocity using a magic factor
|
||||
// The factor is adjusted so that the tension arm is reasonably high when a standard spool is at its lowest diameter
|
||||
|
||||
// Respect the configured max speed limits by taking the minimum of dynamic calculation and configured max
|
||||
let max_speed = self.max_speed();
|
||||
|
||||
// calculate filament tension
|
||||
let tension_arm_angle = tension_arm.get_angle();
|
||||
let tension_arm_revolution = clamp_revolution_uom(
|
||||
tension_arm_angle,
|
||||
// inverted because min angle is max tension
|
||||
@@ -82,20 +120,24 @@ impl MinMaxSpeedController
|
||||
self.filament_calc.get_min_angle(),
|
||||
);
|
||||
|
||||
// if value was clamped return min speed.
|
||||
// why? Idk ask guy who left
|
||||
if matches!(tension_arm_revolution.clamp, Clamp::Min | Clamp::Max) {
|
||||
return min_speed;
|
||||
}
|
||||
match tension_arm_revolution.1 {
|
||||
Clamping::Min => return min_speed,
|
||||
Clamping::Max => return min_speed,
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let filament_tension = self
|
||||
.filament_calc
|
||||
.calc_filament_tension(tension_arm_revolution.value);
|
||||
.calc_filament_tension(tension_arm_revolution.0);
|
||||
|
||||
let filament_tension_inverted = 1.0 - filament_tension;
|
||||
|
||||
// use exponetial interpolation to make the speed change more sensitive in the lower range
|
||||
let filament_tension_exponential = interpolate_exponential(filament_tension_inverted, 2.0);
|
||||
|
||||
// interpolate speed linear
|
||||
|
||||
// save speed
|
||||
AngularVelocity::new::<radian_per_second>(scale(
|
||||
filament_tension_exponential,
|
||||
min_speed.get::<radian_per_second>(),
|
||||
@@ -103,6 +145,14 @@ impl MinMaxSpeedController
|
||||
))
|
||||
}
|
||||
|
||||
/// Accelerates the speed using the acceleration controller.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `speed`: The current speed
|
||||
/// - `t`: The current time.
|
||||
///
|
||||
/// Returns:
|
||||
/// - The new speed after applying acceleration.
|
||||
fn accelerate_speed(&mut self, speed: AngularVelocity, t: Instant) -> AngularVelocity {
|
||||
// The min/mac acceleration depends on the max speed of the last 5secs or the target speed (whatever is higher)
|
||||
let acceleration = AngularAcceleration::new::<radian_per_second_squared>(
|
||||
@@ -129,6 +179,13 @@ impl MinMaxSpeedController
|
||||
new_speed
|
||||
}
|
||||
|
||||
/// Clamps the speed to the defined minimum and maximum speed.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `speed`: The speed to be clamped.
|
||||
///
|
||||
/// Returns:
|
||||
/// - The clamped speed, ensuring it is within the range of `min_speed` and `max_speed`.
|
||||
fn clamp_speed(&mut self, speed: AngularVelocity) -> AngularVelocity {
|
||||
let min_speed = self.min_speed();
|
||||
let max_speed = self.max_speed();
|
||||
@@ -143,8 +200,34 @@ impl MinMaxSpeedController
|
||||
}
|
||||
}
|
||||
|
||||
impl MinMaxSpeedController
|
||||
{
|
||||
impl MinMaxSpoolSpeedController {
|
||||
pub fn update_speed(&mut self, t: Instant, tension_arm: &TensionArm) -> AngularVelocity {
|
||||
let speed = self.speed_raw(t, tension_arm);
|
||||
let speed = match self.enabled {
|
||||
true => speed,
|
||||
false => AngularVelocity::ZERO,
|
||||
};
|
||||
let speed = self.accelerate_speed(speed, t);
|
||||
|
||||
// save speed before clamping or it will stay 0.0
|
||||
self.last_speed = speed;
|
||||
|
||||
self.clamp_speed(speed)
|
||||
}
|
||||
|
||||
pub const fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
pub const fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.last_speed = AngularVelocity::ZERO;
|
||||
self.acceleration_controller.reset(AngularVelocity::ZERO);
|
||||
}
|
||||
|
||||
fn update_acceleration(&mut self) -> Result<(), MotionControllerError> {
|
||||
// Set acceleration to 1/4 of the range between min and max speed
|
||||
// The spool will accelerate from min to max speed in 4 seconds
|
||||
@@ -179,36 +262,32 @@ impl MinMaxSpeedController
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn max_speed(&self) -> AngularVelocity {
|
||||
self.acceleration_controller
|
||||
.get_max_speed()
|
||||
.unwrap_or(AngularVelocity::ZERO)
|
||||
pub fn get_max_speed(&self) -> AngularVelocity {
|
||||
self.max_speed()
|
||||
}
|
||||
|
||||
pub fn min_speed(&self) -> AngularVelocity {
|
||||
self.acceleration_controller
|
||||
.get_min_speed()
|
||||
.unwrap_or(AngularVelocity::ZERO)
|
||||
pub fn get_min_speed(&self) -> AngularVelocity {
|
||||
self.min_speed()
|
||||
}
|
||||
|
||||
pub fn get_speed(&self) -> AngularVelocity {
|
||||
self.speed
|
||||
self.last_speed
|
||||
}
|
||||
|
||||
pub fn set_speed(&mut self, speed: AngularVelocity) {
|
||||
self.speed = speed;
|
||||
self.last_speed = speed;
|
||||
// Also update the acceleration controller's current speed to ensure smooth transitions
|
||||
self.acceleration_controller.reset(speed);
|
||||
}
|
||||
|
||||
/// derive the radius from the puller speed and the current angular speed
|
||||
pub fn get_radius(&self, puller: &Puller) -> Length
|
||||
{
|
||||
let puller_speed = puller.output_speed().get::<meter_per_second>();
|
||||
let angular_speed = self.speed.get::<revolution_per_second>();
|
||||
pub fn get_radius(&self, puller_speed_controller: &PullerSpeedController) -> Length {
|
||||
let puller_speed: Velocity = puller_speed_controller.get_target_speed();
|
||||
let angular_speed: AngularVelocity = self.last_speed;
|
||||
|
||||
// Calculate the radius using the formula: radius = speed / angular_speed
|
||||
let radius = puller_speed / angular_speed;
|
||||
let radius =
|
||||
puller_speed.get::<meter_per_second>() / angular_speed.get::<revolution_per_second>();
|
||||
|
||||
// Ensure the radius is a normal number, otherwise default to 0.0
|
||||
let radius = Some(radius).filter(|&n| n.is_normal()).unwrap_or(0.0);
|
||||
@@ -216,54 +295,3 @@ impl MinMaxSpeedController
|
||||
Length::new::<meter>(radius)
|
||||
}
|
||||
}
|
||||
|
||||
impl SpeedController for MinMaxSpeedController
|
||||
{
|
||||
fn speed(&self) -> AngularVelocity {
|
||||
self.speed
|
||||
}
|
||||
|
||||
fn set_speed(&mut self, speed: AngularVelocity) {
|
||||
self.speed = speed;
|
||||
// Also update the acceleration controller's
|
||||
// current speed to ensure smooth transitions
|
||||
self.acceleration_controller.reset(speed);
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn update_speed(
|
||||
&mut self,
|
||||
t: Instant,
|
||||
multiplier: f64,
|
||||
tension_arm: &TensionArm,
|
||||
puller: &Puller
|
||||
) -> AngularVelocity
|
||||
{
|
||||
_ = puller;
|
||||
|
||||
let speed = self.speed_raw(t, tension_arm.get_angle());
|
||||
let speed = match self.enabled
|
||||
{
|
||||
true => speed,
|
||||
false => AngularVelocity::ZERO,
|
||||
};
|
||||
let speed = self.accelerate_speed(speed * multiplier, t);
|
||||
|
||||
// save speed before clamping or it will stay 0.0
|
||||
self.speed = speed;
|
||||
|
||||
self.clamp_speed(speed)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.speed = AngularVelocity::ZERO;
|
||||
self.acceleration_controller.reset(AngularVelocity::ZERO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use super::Winder2;
|
||||
use crate::MachineAct;
|
||||
use crate::MachineMessage;
|
||||
use crate::MachineValues;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
impl MachineAct for Winder2 {
|
||||
fn act(&mut self, now: Instant) {
|
||||
let msg = self.api_receiver.try_recv();
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
let _res = self.act_machine_message(msg);
|
||||
}
|
||||
Err(_) => (),
|
||||
};
|
||||
// more than 33ms have passed since last emit (30 "fps" target)
|
||||
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
|
||||
self.emit_live_values();
|
||||
self.last_measurement_emit = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
|
||||
match msg {
|
||||
MachineMessage::SubscribeNamespace(namespace) => {
|
||||
self.namespace.namespace = Some(namespace);
|
||||
self.emit_state();
|
||||
tracing::info!("extruder1 received subscribe");
|
||||
}
|
||||
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
use crate::MachineApi;
|
||||
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) => (),
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
MachineMessage::RequestValues(sender) => {
|
||||
sender
|
||||
.send_blocking(MachineValues {
|
||||
state: serde_json::to_value(self.build_state_event())
|
||||
.expect("Failed to serialize state"),
|
||||
live_values: serde_json::to_value(self.get_live_values())
|
||||
.expect("Failed to serialize live values"),
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use super::Winder2;
|
||||
use crate::{MachineApi, winder2::api::Mutation};
|
||||
use serde_json::Value;
|
||||
use std::time::Instant;
|
||||
|
||||
impl MachineApi for Winder2 {
|
||||
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
|
||||
let mutation: Mutation = serde_json::from_value(request_body)?;
|
||||
match mutation {
|
||||
Mutation::EnableTraverseLaserpointer(enable) => self.set_laser(enable),
|
||||
Mutation::SetMode(mode) => self.set_mode(&mode.into()),
|
||||
Mutation::SetTraverseLimitOuter(limit) => self.traverse_set_limit_outer(limit),
|
||||
Mutation::SetTraverseLimitInner(limit) => self.traverse_set_limit_inner(limit),
|
||||
Mutation::SetTraverseStepSize(size) => self.traverse_set_step_size(size),
|
||||
Mutation::SetTraversePadding(padding) => self.traverse_set_padding(padding),
|
||||
Mutation::GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
|
||||
Mutation::GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
|
||||
Mutation::GotoTraverseHome => self.traverse_goto_home(),
|
||||
Mutation::SetPullerRegulationMode(regulation) => self.puller_set_regulation(regulation),
|
||||
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
|
||||
Mutation::SetPullerTargetDiameter(_) => todo!(),
|
||||
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
|
||||
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
|
||||
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
|
||||
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
|
||||
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
|
||||
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
|
||||
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
|
||||
self.spool_set_adaptive_tension_target(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveRadiusLearningRate(value) => {
|
||||
self.spool_set_adaptive_radius_learning_rate(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveMaxSpeedMultiplier(value) => {
|
||||
self.spool_set_adaptive_max_speed_multiplier(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveAccelerationFactor(value) => {
|
||||
self.spool_set_adaptive_acceleration_factor(value)
|
||||
}
|
||||
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
|
||||
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
|
||||
}
|
||||
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
|
||||
self.set_spool_automatic_required_meters(meters)
|
||||
}
|
||||
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
|
||||
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
|
||||
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
|
||||
Mutation::SetConnectedMachine(machine_identification_unique) => {
|
||||
self.set_connected_buffer(machine_identification_unique)
|
||||
}
|
||||
Mutation::DisconnectMachine(machine_identification_unique) => {
|
||||
self.disconnect_buffer(machine_identification_unique)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(
|
||||
&mut self,
|
||||
) -> std::option::Option<control_core::socketio::namespace::Namespace> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
|
||||
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
|
||||
self.api_sender.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use super::Winder2;
|
||||
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
|
||||
use crate::winder2::Winder2Mode;
|
||||
use crate::winder2::api::LiveValuesEvent;
|
||||
use crate::winder2::api::{ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events};
|
||||
use crate::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
|
||||
use crate::winder2::spool_speed_controller::SpoolSpeedControllerType;
|
||||
use crate::{MACHINE_WINDER_V1, VENDOR_QITECH};
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
|
||||
impl Winder2 {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_WINDER_V1,
|
||||
};
|
||||
|
||||
pub fn stop_or_pull_spool_reset(&mut self, _now: Instant) {}
|
||||
/// Implement Mode
|
||||
pub fn set_mode(&mut self, mode: &Winder2Mode) {
|
||||
self.mode_state = ModeState {
|
||||
mode: mode.clone().into(),
|
||||
can_wind: true,
|
||||
};
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Implement Traverse
|
||||
pub fn set_laser(&mut self, value: bool) {
|
||||
self.traverse_state.laserpointer = value;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
|
||||
self.traverse_state.limit_inner = limit;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
|
||||
self.traverse_state.limit_outer = limit;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_step_size(&mut self, step_size: f64) {
|
||||
self.traverse_state.step_size = step_size;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_padding(&mut self, padding: f64) {
|
||||
self.traverse_state.padding = padding;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_inner(&mut self) {
|
||||
self.traverse_state.position_in = self.traverse_state.limit_inner;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_outer(&mut self) {
|
||||
self.traverse_state.position_in = self.traverse_state.limit_outer;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_home(&mut self) {
|
||||
self.traverse_state.position_in = 0.0;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let event = self.get_live_values().build();
|
||||
self.namespace.emit(Winder2Events::LiveValues(event));
|
||||
}
|
||||
|
||||
pub fn get_live_values(&self) -> LiveValuesEvent {
|
||||
LiveValuesEvent {
|
||||
traverse_position: Some(0.0),
|
||||
puller_speed: 0.0,
|
||||
spool_rpm: 0.0,
|
||||
tension_arm_angle: 0.0,
|
||||
spool_progress: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_state_event(&mut self) -> StateEvent {
|
||||
use crate::MachineCrossConnectionState;
|
||||
|
||||
let connected_machine = self.connected_machines.get(0);
|
||||
|
||||
let ident = match connected_machine {
|
||||
Some(machine) => Some(machine.ident.clone()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let cross_conn = MachineCrossConnectionState {
|
||||
machine_identification_unique: ident,
|
||||
is_available: connected_machine.is_some(),
|
||||
};
|
||||
|
||||
StateEvent {
|
||||
is_default_state: self.is_default_state,
|
||||
traverse_state: self.traverse_state.clone(),
|
||||
puller_state: self.puller_state.clone(),
|
||||
spool_automatic_action_state: self.spool_automatic_action_state.clone(),
|
||||
mode_state: self.mode_state.clone(),
|
||||
tension_arm_state: self.tension_arm_state.clone(),
|
||||
spool_speed_controller_state: self.spool_speed_controller_state.clone(),
|
||||
connected_machine_state: cross_conn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self) {
|
||||
let state = self.build_state_event();
|
||||
let event = state.build();
|
||||
self.namespace.emit(Winder2Events::State(event));
|
||||
}
|
||||
|
||||
/// Apply the mode changes to the spool
|
||||
///
|
||||
/// It contains a transition matrix for atomic changes.
|
||||
/// It will set [`Self::spool_mode`]
|
||||
fn set_traverse_mode(&mut self, _mode: &Winder2Mode) {
|
||||
self.mode_state = ModeState {
|
||||
mode: crate::winder2::api::Mode::Standby,
|
||||
can_wind: true,
|
||||
};
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Implement Tension Arm
|
||||
pub fn tension_arm_zero(&mut self) {
|
||||
self.tension_arm_state.zeroed = true;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
|
||||
self.spool_automatic_action_state.spool_required_meters = meters;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
|
||||
self.spool_automatic_action_state
|
||||
.spool_automatic_action_mode = mode;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
|
||||
self.puller_state.regulation = puller_regulation_mode;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set target speed in m/min
|
||||
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
|
||||
self.puller_state.target_speed = target_speed;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set target diameter in mm
|
||||
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
|
||||
self.puller_state.target_diameter = target_diameter;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set forward direction
|
||||
pub fn puller_set_forward(&mut self, forward: bool) {
|
||||
self.puller_state.forward = forward;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set gear ratio for winding speed
|
||||
pub fn puller_set_gear_ratio(&mut self, gear_ratio: GearRatio) {
|
||||
self.puller_state.gear_ratio = gear_ratio;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
// Spool Speed Controller API methods
|
||||
pub fn spool_set_regulation_mode(&mut self, regulation_mode: SpoolSpeedControllerType) {
|
||||
self.spool_speed_controller_state.regulation_mode = regulation_mode;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set minimum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
|
||||
self.spool_speed_controller_state.minmax_min_speed = min_speed_rpm;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set maximum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
|
||||
self.spool_speed_controller_state.minmax_max_speed = max_speed_rpm;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set tension target for adaptive mode (0.0-1.0)
|
||||
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
|
||||
self.spool_speed_controller_state.adaptive_tension_target = tension_target;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set radius learning rate for adaptive mode
|
||||
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.spool_speed_controller_state
|
||||
.adaptive_radius_learning_rate = radius_learning_rate;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set max speed multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.spool_speed_controller_state
|
||||
.adaptive_max_speed_multiplier = max_speed_multiplier;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set acceleration factor for adaptive mode
|
||||
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.spool_speed_controller_state
|
||||
.adaptive_acceleration_factor = acceleration_factor;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set deacceleration urgency multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.spool_speed_controller_state
|
||||
.adaptive_deacceleration_urgency_multiplier = deacceleration_urgency_multiplier;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set forward rotation direction
|
||||
pub fn spool_set_forward(&mut self, forward: bool) {
|
||||
self.spool_speed_controller_state.forward = forward;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// implement machine connection
|
||||
/// set connected buffer
|
||||
pub fn set_connected_buffer(
|
||||
&mut self,
|
||||
_machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// disconnect buffer
|
||||
pub fn disconnect_buffer(
|
||||
&mut self,
|
||||
_machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use std::time::Instant;
|
||||
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod mock_emit;
|
||||
pub mod new;
|
||||
|
||||
use super::api::{
|
||||
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState, TensionArmState,
|
||||
TraverseState, Winder2Namespace,
|
||||
};
|
||||
use crate::{
|
||||
AsyncThreadMessage, Machine, MachineConnection, MachineMessage,
|
||||
machine_identification::MachineIdentificationUnique,
|
||||
};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Winder2 {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
namespace: Winder2Namespace,
|
||||
last_measurement_emit: Instant,
|
||||
pub is_default_state: bool,
|
||||
/// traverse state
|
||||
pub traverse_state: TraverseState,
|
||||
/// puller state
|
||||
pub puller_state: PullerState,
|
||||
/// spool automatic action state and progress
|
||||
pub spool_automatic_action_state: SpoolAutomaticActionState,
|
||||
/// mode state
|
||||
pub mode_state: ModeState,
|
||||
/// tension arm state
|
||||
pub tension_arm_state: TensionArmState,
|
||||
/// spool speed controller state
|
||||
pub spool_speed_controller_state: SpoolSpeedControllerState,
|
||||
|
||||
/// Receive from Api or MainThread
|
||||
api_receiver: Receiver<MachineMessage>,
|
||||
api_sender: Sender<MachineMessage>,
|
||||
|
||||
/// Communicate with main thread
|
||||
main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
|
||||
/// All currently "connected" Machines
|
||||
connected_machines: Vec<MachineConnection>,
|
||||
/// Defaults to limit of 2
|
||||
max_connected_machines: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Winder2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Winder2")
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for Winder2 {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
|
||||
self.main_sender.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::{
|
||||
MachineNewParams, MachineNewTrait,
|
||||
winder2::api::{
|
||||
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState,
|
||||
TensionArmState, TraverseState, Winder2Namespace,
|
||||
},
|
||||
};
|
||||
|
||||
use super::Winder2;
|
||||
|
||||
impl MachineNewTrait for Winder2 {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// Mock machine can work with either Serial or Ethercat hardware
|
||||
// For the mock machine, we don't need to actually use the hardware
|
||||
// We just validate that we have the expected hardware type
|
||||
match params.hardware {
|
||||
crate::MachineNewHardware::Serial(_) => {
|
||||
// For serial mode, we could potentially use the serial device if needed
|
||||
// but for a mock machine, we'll just note it and proceed
|
||||
}
|
||||
crate::MachineNewHardware::Ethercat(_) => {
|
||||
// For ethercat mode, we could potentially use the ethercat devices
|
||||
// but for a mock machine, we'll just note it and proceed
|
||||
}
|
||||
}
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut winder_mock_machine = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
max_connected_machines: 2,
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: Winder2Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_measurement_emit: now,
|
||||
is_default_state: true,
|
||||
traverse_state: TraverseState::default(),
|
||||
puller_state: PullerState::default(),
|
||||
spool_automatic_action_state: SpoolAutomaticActionState::default(),
|
||||
mode_state: ModeState::default(),
|
||||
tension_arm_state: TensionArmState::default(),
|
||||
spool_speed_controller_state: SpoolSpeedControllerState::default(),
|
||||
connected_machines: vec![],
|
||||
};
|
||||
|
||||
winder_mock_machine.emit_state();
|
||||
|
||||
Ok(winder_mock_machine)
|
||||
}
|
||||
}
|
||||
+430
-209
@@ -1,237 +1,458 @@
|
||||
use std::time::{Duration, Instant};
|
||||
pub mod act;
|
||||
pub mod adaptive_spool_speed_controller;
|
||||
pub mod api;
|
||||
pub mod clamp_revolution;
|
||||
pub mod emit;
|
||||
pub mod filament_tension;
|
||||
pub mod minmax_spool_speed_controller;
|
||||
pub mod new;
|
||||
pub mod puller_speed_controller;
|
||||
pub mod spool_speed_controller;
|
||||
pub mod tension_arm;
|
||||
pub mod traverse_controller;
|
||||
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub mod mock;
|
||||
|
||||
use crate::{
|
||||
MachineChannel,
|
||||
MachineWithChannel,
|
||||
MachineConnection,
|
||||
MachinesLiveValues,
|
||||
VENDOR_QITECH,
|
||||
machine_identification::MachineIdentification
|
||||
};
|
||||
|
||||
mod types;
|
||||
use types::{Mode, Hardware, SpoolLengthTaskCompletedAction};
|
||||
|
||||
mod tasks;
|
||||
use tasks::SpoolLengthTask;
|
||||
|
||||
mod devices;
|
||||
use devices::{Spool, Traverse, Puller, TensionArm};
|
||||
|
||||
mod api;
|
||||
use api::{LiveValues, State};
|
||||
|
||||
mod new;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Winder2
|
||||
{
|
||||
// common machine fields
|
||||
channel: MachineChannel,
|
||||
last_emit: Instant,
|
||||
emitted_default_state: bool,
|
||||
|
||||
// machine config
|
||||
mode: Mode,
|
||||
on_spool_length_task_complete: SpoolLengthTaskCompletedAction,
|
||||
|
||||
// devices
|
||||
spool: Spool,
|
||||
puller: Puller,
|
||||
traverse: Traverse,
|
||||
tension_arm: TensionArm,
|
||||
|
||||
// tasks
|
||||
spool_length_task: SpoolLengthTask,
|
||||
|
||||
// reference machine for the pullers adaptive speed mode
|
||||
puller_speed_reference_machine: Option<MachineConnection>,
|
||||
#[cfg(feature = "mock-machine")]
|
||||
mod winder2_imports {
|
||||
pub use super::api::SpoolAutomaticActionMode;
|
||||
pub use std::time::Instant;
|
||||
pub use units::f64::Length;
|
||||
pub use units::length::meter;
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl Winder2
|
||||
{
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification
|
||||
{
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
mod winder2_imports {
|
||||
pub use super::api::SpoolAutomaticActionMode;
|
||||
pub use super::api::Winder2Namespace;
|
||||
pub use super::puller_speed_controller::PullerSpeedController;
|
||||
pub use super::spool_speed_controller::SpoolSpeedController;
|
||||
pub use super::tension_arm::TensionArm;
|
||||
pub use super::traverse_controller::TraverseController;
|
||||
pub use control_core::converters::angular_step_converter::AngularStepConverter;
|
||||
pub use ethercat_hal::io::{
|
||||
digital_input::DigitalInput, digital_output::DigitalOutput,
|
||||
stepper_velocity_el70x1::StepperVelocityEL70x1,
|
||||
};
|
||||
pub use smol::channel::{Receiver, Sender};
|
||||
pub use smol::lock::RwLock;
|
||||
pub use std::{fmt::Debug, sync::Weak, time::Instant};
|
||||
|
||||
pub use crate::buffer1::BufferV1;
|
||||
pub use crate::{AsyncThreadMessage, Machine};
|
||||
pub use units::ConstZero;
|
||||
pub use units::f64::Length;
|
||||
pub use units::{length::meter, length::millimeter, velocity::meter_per_second};
|
||||
}
|
||||
|
||||
pub use winder2_imports::*;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{
|
||||
MACHINE_WINDER_V1, MachineConnection, MachineMessage, VENDOR_QITECH,
|
||||
machine_identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpoolAutomaticAction {
|
||||
pub progress: Length,
|
||||
progress_last_check: Instant,
|
||||
pub target_length: Length,
|
||||
pub mode: SpoolAutomaticActionMode,
|
||||
}
|
||||
|
||||
impl Default for SpoolAutomaticAction {
|
||||
fn default() -> Self {
|
||||
SpoolAutomaticAction {
|
||||
progress: Length::new::<meter>(0.0),
|
||||
progress_last_check: Instant::now(),
|
||||
target_length: Length::new::<meter>(0.0),
|
||||
mode: SpoolAutomaticActionMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl Machine for Winder2 {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
|
||||
self.main_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
#[derive(Debug)]
|
||||
pub struct Winder2 {
|
||||
api_receiver: Receiver<MachineMessage>,
|
||||
api_sender: Sender<MachineMessage>,
|
||||
main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
|
||||
connected_machines: Vec<MachineConnection>,
|
||||
max_connected_machines: usize,
|
||||
// drivers
|
||||
pub traverse: StepperVelocityEL70x1,
|
||||
pub puller: StepperVelocityEL70x1,
|
||||
pub spool: StepperVelocityEL70x1,
|
||||
pub tension_arm: TensionArm,
|
||||
pub laser: DigitalOutput,
|
||||
|
||||
// controllers
|
||||
pub traverse_controller: TraverseController,
|
||||
pub traverse_end_stop: DigitalInput,
|
||||
|
||||
// socketio
|
||||
namespace: Winder2Namespace,
|
||||
last_measurement_emit: Instant,
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// mode
|
||||
pub mode: Winder2Mode,
|
||||
pub spool_mode: SpoolMode,
|
||||
pub traverse_mode: TraverseMode,
|
||||
pub puller_mode: PullerMode,
|
||||
|
||||
// control circuit arm/spool
|
||||
pub spool_speed_controller: SpoolSpeedController,
|
||||
pub spool_step_converter: AngularStepConverter,
|
||||
|
||||
// spool automatic action state
|
||||
pub spool_automatic_action: SpoolAutomaticAction,
|
||||
|
||||
// control circuit puller
|
||||
pub puller_speed_controller: PullerSpeedController,
|
||||
|
||||
/// Will be initialized as false and set to true by emit_state
|
||||
/// This way we can signal to the client that the first state emission is a default state
|
||||
emitted_default_state: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl Winder2 {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: crate::MACHINE_WINDER_V1,
|
||||
machine: MACHINE_WINDER_V1,
|
||||
};
|
||||
|
||||
pub fn new(channel: MachineChannel, hardware: Hardware) -> Self
|
||||
{
|
||||
let traverse = Traverse::new(
|
||||
hardware.traverse_motor,
|
||||
hardware.traverse_limit_switch,
|
||||
hardware.traverse_laser_pointer,
|
||||
);
|
||||
/// Validates that traverse limits maintain proper constraints:
|
||||
/// - Inner limit must be smaller than outer limit
|
||||
/// - At least 0.9mm difference between inner and outer limits
|
||||
fn validate_traverse_limits(inner: Length, outer: Length) -> bool {
|
||||
outer > inner + Length::new::<millimeter>(0.9)
|
||||
}
|
||||
|
||||
Self {
|
||||
channel,
|
||||
last_emit: Instant::now(),
|
||||
emitted_default_state: false,
|
||||
|
||||
mode: Mode::Standby,
|
||||
spool: Spool::new(hardware.spool_motor),
|
||||
puller: Puller::new(hardware.puller_motor),
|
||||
traverse,
|
||||
tension_arm: TensionArm::new(hardware.tension_arm_sensor),
|
||||
spool_length_task: SpoolLengthTask::new(),
|
||||
on_spool_length_task_complete: SpoolLengthTaskCompletedAction::NoAction,
|
||||
puller_speed_reference_machine: None,
|
||||
}
|
||||
pub fn sync_traverse_speed(&mut self) {
|
||||
self.traverse_controller.update_speed(
|
||||
&mut self.traverse,
|
||||
&self.traverse_end_stop,
|
||||
self.spool_speed_controller.get_speed(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Can wind capability check
|
||||
fn can_wind(&self) -> bool
|
||||
{
|
||||
// Check if tension arm is calibrated and traverse is homed
|
||||
self.tension_arm.is_calibrated()
|
||||
&& self.traverse.is_homed()
|
||||
&& !self.traverse.is_going_home()
|
||||
pub const fn can_wind(&self) -> bool {
|
||||
// Check if tension arm is zeroed and traverse is homed
|
||||
self.tension_arm.zeroed
|
||||
&& self.traverse_controller.is_homed()
|
||||
&& !self.traverse_controller.is_going_home()
|
||||
}
|
||||
|
||||
// TODO(JSE): why the 2 checks for traverse?
|
||||
/// Can go to inner limit capability check
|
||||
pub fn can_go_in(&self) -> bool {
|
||||
// Check if traverse is homed, not in standby, not traversing
|
||||
// Allow changing direction (even when going out)
|
||||
// Disallow when homing is in progress
|
||||
self.traverse_controller.is_homed()
|
||||
&& self.traverse_mode != TraverseMode::Standby
|
||||
&& !self.traverse_controller.is_going_in()
|
||||
&& !self.traverse_controller.is_going_home()
|
||||
&& !self.traverse_controller.is_traversing()
|
||||
&& self.mode != Winder2Mode::Wind
|
||||
}
|
||||
|
||||
/// Can go to outer limit capability check
|
||||
pub fn can_go_out(&self) -> bool {
|
||||
// Check if traverse is homed, not in standby, not traversing
|
||||
// Allow changing direction (even when going in)
|
||||
// Disallow when homing is in progress
|
||||
self.traverse_controller.is_homed()
|
||||
&& self.traverse_mode != TraverseMode::Standby
|
||||
&& !self.traverse_controller.is_going_out()
|
||||
&& !self.traverse_controller.is_going_home()
|
||||
&& !self.traverse_controller.is_traversing()
|
||||
&& self.mode != Winder2Mode::Wind
|
||||
}
|
||||
|
||||
/// Can go home capability check
|
||||
pub fn can_go_home(&self) -> bool {
|
||||
// Check if not in standby, not traversing
|
||||
// Allow going home even when going in or out
|
||||
self.traverse_mode != TraverseMode::Standby
|
||||
&& !self.traverse_controller.is_going_home()
|
||||
&& !self.traverse_controller.is_traversing()
|
||||
&& self.mode != Winder2Mode::Wind
|
||||
}
|
||||
|
||||
/// Apply the mode changes to the spool
|
||||
///
|
||||
/// It contains a transition matrix for atomic changes.
|
||||
/// It will set [`Self::spool_mode`]
|
||||
fn set_spool_mode(&mut self, mode: &Winder2Mode) {
|
||||
// Convert to `Winder2Mode` to `SpoolMode`
|
||||
let mode: SpoolMode = mode.clone().into();
|
||||
|
||||
// Transition matrix
|
||||
match self.spool_mode {
|
||||
SpoolMode::Standby => match mode {
|
||||
SpoolMode::Standby => {}
|
||||
SpoolMode::Hold => {
|
||||
// From [`SpoolMode::Standby`] to [`SpoolMode::Hold`]
|
||||
self.spool.set_enabled(true);
|
||||
}
|
||||
SpoolMode::Wind => {
|
||||
self.spool.set_enabled(true);
|
||||
// self.spool_speed_controller.reset();
|
||||
self.spool_speed_controller.set_enabled(true);
|
||||
}
|
||||
},
|
||||
SpoolMode::Hold => match mode {
|
||||
SpoolMode::Standby => {
|
||||
// From [`SpoolMode::Hold`] to [`SpoolMode::Standby`]
|
||||
self.spool.set_enabled(false);
|
||||
}
|
||||
SpoolMode::Hold => {}
|
||||
SpoolMode::Wind => {
|
||||
// From [`SpoolMode::Hold`] to [`SpoolMode::Wind`]
|
||||
// self.spool_speed_controller.reset();
|
||||
self.spool_speed_controller.set_enabled(true);
|
||||
}
|
||||
},
|
||||
SpoolMode::Wind => match mode {
|
||||
SpoolMode::Standby => {
|
||||
// From [`SpoolMode::Wind`] to [`SpoolMode::Standby`]
|
||||
self.spool.set_enabled(false);
|
||||
self.spool_speed_controller.set_enabled(false);
|
||||
}
|
||||
SpoolMode::Hold => {
|
||||
// From [`SpoolMode::Wind`] to [`SpoolMode::Hold`]
|
||||
self.spool_speed_controller.set_enabled(false);
|
||||
}
|
||||
SpoolMode::Wind => {}
|
||||
},
|
||||
}
|
||||
|
||||
// Update the internal state
|
||||
self.spool_mode = mode;
|
||||
}
|
||||
|
||||
/// Apply the mode changes to the puller
|
||||
///
|
||||
/// It contains a transition matrix for atomic changes.
|
||||
/// It will set [`Self::puller_mode`]
|
||||
fn set_puller_mode(&mut self, mode: &Winder2Mode) {
|
||||
// Convert to `Winder2Mode` to `PullerMode`
|
||||
let mode: PullerMode = mode.clone().into();
|
||||
|
||||
// Transition matrix
|
||||
match self.puller_mode {
|
||||
PullerMode::Standby => match mode {
|
||||
PullerMode::Standby => {}
|
||||
PullerMode::Hold => {
|
||||
// From [`PullerMode::Standby`] to [`PullerMode::Hold`]
|
||||
self.puller.set_enabled(true);
|
||||
}
|
||||
PullerMode::Pull => {
|
||||
// From [`PullerMode::Standby`] to [`PullerMode::Pull`]
|
||||
self.puller.set_enabled(true);
|
||||
self.puller_speed_controller.set_enabled(true);
|
||||
}
|
||||
},
|
||||
PullerMode::Hold => match mode {
|
||||
PullerMode::Standby => {
|
||||
// From [`PullerMode::Hold`] to [`PullerMode::Standby`]
|
||||
self.puller.set_enabled(false);
|
||||
}
|
||||
PullerMode::Hold => {}
|
||||
PullerMode::Pull => {
|
||||
// From [`PullerMode::Hold`] to [`PullerMode::Pull`]
|
||||
self.puller_speed_controller.set_enabled(true);
|
||||
}
|
||||
},
|
||||
PullerMode::Pull => match mode {
|
||||
PullerMode::Standby => {
|
||||
// From [`PullerMode::Pull`] to [`PullerMode::Standby`]
|
||||
self.puller.set_enabled(false);
|
||||
self.puller_speed_controller.set_enabled(false);
|
||||
}
|
||||
PullerMode::Hold => {
|
||||
// From [`PullerMode::Pull`] to [`PullerMode::Hold`]
|
||||
self.puller_speed_controller.set_enabled(false);
|
||||
}
|
||||
PullerMode::Pull => {}
|
||||
},
|
||||
}
|
||||
|
||||
// Update the internal state
|
||||
self.puller_mode = mode;
|
||||
}
|
||||
|
||||
pub const fn stop_or_pull_spool_reset(&mut self, now: Instant) {
|
||||
self.spool_automatic_action.progress = Length::ZERO;
|
||||
self.spool_automatic_action.progress_last_check = now;
|
||||
}
|
||||
|
||||
pub fn calculate_spool_auto_progress_(&mut self, now: Instant) {
|
||||
// Calculate time elapsed since last progress check (in minutes)
|
||||
|
||||
let dt = now
|
||||
.duration_since(self.spool_automatic_action.progress_last_check)
|
||||
.as_secs_f64();
|
||||
|
||||
// Calculate distance pulled during this time interval
|
||||
let meters_pulled_this_interval = Length::new::<meter>(
|
||||
self.puller_speed_controller
|
||||
.last_speed
|
||||
.get::<meter_per_second>()
|
||||
* dt,
|
||||
);
|
||||
|
||||
// Update total meters pulled
|
||||
self.spool_automatic_action.progress += meters_pulled_this_interval.abs();
|
||||
self.spool_automatic_action.progress_last_check = now;
|
||||
}
|
||||
|
||||
/// Implement Puller
|
||||
/// called by `act`
|
||||
pub fn sync_puller_speed(&mut self, t: Instant) {
|
||||
let angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
|
||||
let steps_per_second = self
|
||||
.puller_speed_controller
|
||||
.converter
|
||||
.angular_velocity_to_steps(angular_velocity);
|
||||
let _ = self.puller.set_speed(steps_per_second);
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
impl Winder2
|
||||
{
|
||||
/// Updates the spool length task and changes the
|
||||
/// machines mode as appropiate
|
||||
fn update_spool_length_task(&mut self, now: Instant)
|
||||
{
|
||||
// TODO(JSE): find out if update_timer can be replaced
|
||||
// with update_progress(), since speed should be 0
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub use mock::Winder2;
|
||||
|
||||
// refactor of: stop_or_pull_spool
|
||||
use SpoolLengthTaskCompletedAction::*;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Winder2Mode {
|
||||
Standby,
|
||||
Hold,
|
||||
Pull,
|
||||
Wind,
|
||||
}
|
||||
|
||||
let velocity = self.puller.output_speed();
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SpoolMode {
|
||||
Standby,
|
||||
Hold,
|
||||
Wind,
|
||||
}
|
||||
|
||||
// always update progress if no action
|
||||
if self.on_spool_length_task_complete == NoAction
|
||||
{
|
||||
self.spool_length_task.update_progress(now, velocity);
|
||||
return;
|
||||
impl From<Winder2Mode> for SpoolMode {
|
||||
fn from(mode: Winder2Mode) -> Self {
|
||||
match mode {
|
||||
Winder2Mode::Standby => Self::Standby,
|
||||
Winder2Mode::Hold => Self::Hold,
|
||||
Winder2Mode::Pull => Self::Hold,
|
||||
Winder2Mode::Wind => Self::Wind,
|
||||
}
|
||||
|
||||
// don't update progress if no mode to move the puller
|
||||
// is active...
|
||||
//
|
||||
// jse: But why even bother, if it ain't moving anyway?
|
||||
// if the motor doesn't stop immediately won't we have
|
||||
// a false value then since we stop counting while
|
||||
// it slows down??
|
||||
if self.mode != Mode::Pull && self.mode != Mode::Wind
|
||||
{
|
||||
self.spool_length_task.update_timer(now);
|
||||
return;
|
||||
}
|
||||
|
||||
self.spool_length_task.update_progress(now, velocity);
|
||||
|
||||
if self.spool_length_task.is_complete()
|
||||
{
|
||||
let mode = match self.on_spool_length_task_complete
|
||||
{
|
||||
Pull => Mode::Pull,
|
||||
Hold => Mode::Hold,
|
||||
NoAction => return, // unreachable
|
||||
};
|
||||
|
||||
self.spool_length_task.reset(now);
|
||||
self.set_mode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_state(&mut self)
|
||||
{
|
||||
let event = self.get_state();
|
||||
self.emitted_default_state = true;
|
||||
self.channel.emit(event);
|
||||
}
|
||||
|
||||
fn emit_live_values(&mut self)
|
||||
{
|
||||
let event = self.create_live_values();
|
||||
self.channel.emit(event);
|
||||
self.last_emit = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
// base machine trait
|
||||
impl MachineWithChannel for Winder2
|
||||
{
|
||||
type State = State;
|
||||
type LiveValues = LiveValues;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TraverseMode {
|
||||
Standby,
|
||||
Hold,
|
||||
Traverse,
|
||||
}
|
||||
|
||||
fn update(&mut self, now: std::time::Instant) -> anyhow::Result<()>
|
||||
{
|
||||
let prev_traverse_state = self.traverse.state();
|
||||
|
||||
self.spool.update(now, &self.tension_arm, &self.puller);
|
||||
self.puller.update(now);
|
||||
self.traverse.update(&self.spool);
|
||||
|
||||
// update last since it can mutate mode
|
||||
self.update_spool_length_task(now);
|
||||
|
||||
if prev_traverse_state != self.traverse.state()
|
||||
{
|
||||
// traverse state changed, emit state change
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
if now.duration_since(self.last_emit) > Duration::from_secs_f64(1.0 / 30.0)
|
||||
{
|
||||
self.emit_state();
|
||||
self.emit_live_values();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_state(&self) -> State
|
||||
{
|
||||
self.create_state()
|
||||
}
|
||||
|
||||
fn get_live_values(&self) -> Option<LiveValues>
|
||||
{
|
||||
Some(self.create_live_values())
|
||||
}
|
||||
|
||||
fn mutate(&mut self, value: serde_json::Value) -> anyhow::Result<()>
|
||||
{
|
||||
self.handle_mutation(value)
|
||||
}
|
||||
|
||||
fn on_namespace(&mut self)
|
||||
{
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
fn on_receive_live_values(&mut self, live_values: MachinesLiveValues)
|
||||
{
|
||||
use crate::MachinesLiveValues::Laser;
|
||||
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Laser(live_values) = live_values
|
||||
{
|
||||
_ = live_values;
|
||||
// TODO: use data to regulate speed. But not idea how, when what?
|
||||
impl From<Winder2Mode> for TraverseMode {
|
||||
fn from(mode: Winder2Mode) -> Self {
|
||||
match mode {
|
||||
Winder2Mode::Standby => Self::Standby,
|
||||
Winder2Mode::Hold => Self::Hold,
|
||||
Winder2Mode::Pull => Self::Hold,
|
||||
Winder2Mode::Wind => Self::Traverse,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_machine_channel(&self) -> &MachineChannel
|
||||
{
|
||||
&self.channel
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PullerMode {
|
||||
Standby,
|
||||
Hold,
|
||||
Pull,
|
||||
}
|
||||
|
||||
fn get_machine_channel_mut(&mut self) -> &mut MachineChannel
|
||||
{
|
||||
&mut self.channel
|
||||
impl From<Winder2Mode> for PullerMode {
|
||||
fn from(mode: Winder2Mode) -> Self {
|
||||
match mode {
|
||||
Winder2Mode::Standby => Self::Standby,
|
||||
Winder2Mode::Hold => Self::Hold,
|
||||
Winder2Mode::Pull => Self::Pull,
|
||||
Winder2Mode::Wind => Self::Pull,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl std::fmt::Display for Winder2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Winder2")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_traverse_limits() {
|
||||
// Test case 1: Valid limits with exactly 1.0mm difference (should pass)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(16.0);
|
||||
assert!(Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 2: Invalid limits with exactly 0.9mm difference (should fail)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.9);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 3: Invalid limits with less than 0.9mm difference (should fail)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.5);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 4: Invalid limits where inner equals outer (should fail)
|
||||
let inner = Length::new::<millimeter>(20.0);
|
||||
let outer = Length::new::<millimeter>(20.0);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 5: Invalid limits where inner is greater than outer (should fail)
|
||||
let inner = Length::new::<millimeter>(25.0);
|
||||
let outer = Length::new::<millimeter>(20.0);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 6: Valid limits with large difference (should pass)
|
||||
let inner = Length::new::<millimeter>(10.0);
|
||||
let outer = Length::new::<millimeter>(80.0);
|
||||
assert!(Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 7: Edge case - exactly 0.91mm difference (should pass)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.91);
|
||||
assert!(Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 8: Edge case - exactly 0.89mm difference (should fail)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.89);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
}
|
||||
}
|
||||
+127
-47
@@ -1,21 +1,62 @@
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::{coe::ConfigurableDevice, devices::{ek1100::{EK1100, EK1100_IDENTITY_A}, el2002::{EL2002, EL2002_IDENTITY_A, EL2002_IDENTITY_B, EL2002Port}, el7031::{EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort, coe::EL7031Configuration, pdo::EL7031PredefinedPdoAssignment}, el7031_0030::{self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort, EL7031_0030StepperPort, coe::EL7031_0030Configuration, pdo::EL7031_0030PredefinedPdoAssignment}, el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port, coe::EL7041_0052Configuration}}, io::{analog_input::AnalogInput, digital_input::DigitalInput, digital_output::DigitalOutput, stepper_velocity_el70x1::StepperVelocityEL70x1}, shared_config::{self, el70x1::{EL70x1OperationMode, StmMotorConfiguration}}};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
mod winder2_imports {
|
||||
pub use super::super::api::Winder2Namespace;
|
||||
pub use super::super::tension_arm::TensionArm;
|
||||
pub use super::super::{Winder2, Winder2Mode};
|
||||
pub use crate::winder2::puller_speed_controller::PullerSpeedController;
|
||||
pub use crate::winder2::spool_speed_controller::SpoolSpeedController;
|
||||
pub use crate::winder2::traverse_controller::TraverseController;
|
||||
pub use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
pub use anyhow::Error;
|
||||
pub use control_core::converters::angular_step_converter::AngularStepConverter;
|
||||
pub use control_core::converters::linear_step_converter::LinearStepConverter;
|
||||
|
||||
use crate::{MachineChannel, get_ethercat_device, winder2::types::Hardware};
|
||||
pub use crate::{
|
||||
MachineNewHardware,
|
||||
MachineNewParams,
|
||||
MachineNewTrait,
|
||||
validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
pub use ethercat_hal::coe::ConfigurableDevice;
|
||||
pub use ethercat_hal::devices::ek1100::EK1100;
|
||||
pub use ethercat_hal::devices::el2002::{EL2002, EL2002_IDENTITY_B, EL2002Port};
|
||||
pub use ethercat_hal::devices::el7031::coe::EL7031Configuration;
|
||||
pub use ethercat_hal::devices::el7031::pdo::EL7031PredefinedPdoAssignment;
|
||||
pub use ethercat_hal::devices::el7031::{
|
||||
EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort,
|
||||
};
|
||||
pub use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
|
||||
pub use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
|
||||
pub use ethercat_hal::devices::el7031_0030::{
|
||||
self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort,
|
||||
EL7031_0030StepperPort,
|
||||
};
|
||||
pub use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
|
||||
pub use ethercat_hal::devices::el7041_0052::{
|
||||
EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port,
|
||||
};
|
||||
pub use ethercat_hal::devices::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
|
||||
pub use ethercat_hal::io::analog_input::AnalogInput;
|
||||
pub use ethercat_hal::io::digital_input::DigitalInput;
|
||||
pub use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
pub use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
|
||||
pub use ethercat_hal::shared_config;
|
||||
pub use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
|
||||
pub use std::time::Instant;
|
||||
pub use units::ConstZero;
|
||||
pub use units::f64::*;
|
||||
pub use units::length::{centimeter, meter, millimeter};
|
||||
pub use units::velocity::meter_per_minute;
|
||||
}
|
||||
|
||||
use super::Winder2;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub use winder2_imports::*;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::get_ethercat_device;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineNewTrait for Winder2 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
|
||||
impl MachineNewTrait for Winder2
|
||||
{
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error>
|
||||
{
|
||||
let device_identification = params.device_group.to_vec();
|
||||
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
@@ -31,9 +72,10 @@ impl MachineNewTrait for Winder2
|
||||
}
|
||||
};
|
||||
|
||||
// map the required hardware devices
|
||||
let (el2002, el7041, el7031, el7031_0030) = smol::block_on(async
|
||||
{
|
||||
// using block_on because making this funciton async creates a lifetime issue
|
||||
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
|
||||
// so we can't drop subdevices unless this machine is dropped, which is bad
|
||||
smol::block_on(async {
|
||||
// Role 0: Buscoupler EK1100
|
||||
let _ek1100 =
|
||||
get_ethercat_device::<EK1100>(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?;
|
||||
@@ -44,7 +86,9 @@ impl MachineNewTrait for Winder2
|
||||
params,
|
||||
1,
|
||||
vec![EL2002_IDENTITY_A, EL2002_IDENTITY_B],
|
||||
).await?.0;
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
|
||||
// Role 2: Stepper Spool EL7041-0052
|
||||
let el7041 = {
|
||||
@@ -74,7 +118,6 @@ impl MachineNewTrait for Winder2
|
||||
.await
|
||||
.write_config(&device.1, &el7041_config)
|
||||
.await?;
|
||||
|
||||
device.0
|
||||
};
|
||||
|
||||
@@ -141,36 +184,73 @@ impl MachineNewTrait for Winder2
|
||||
.await
|
||||
.write_config(&device.1, &el7031_0030_config)
|
||||
.await?;
|
||||
|
||||
device.0
|
||||
};
|
||||
|
||||
Ok::<_, anyhow::Error>((el2002, el7041, el7031, el7031_0030))
|
||||
})?;
|
||||
let mode = Winder2Mode::Standby;
|
||||
|
||||
let machine_id = params
|
||||
.device_group
|
||||
.first()
|
||||
.expect("device group must have at least one device")
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
let machine_id = params
|
||||
.device_group
|
||||
.first()
|
||||
.expect("device group must have at least one device")
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut new = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
max_connected_machines: 2,
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
traverse: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
|
||||
traverse_end_stop: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
|
||||
puller: StepperVelocityEL70x1::new(
|
||||
el7031_0030.clone(),
|
||||
EL7031_0030StepperPort::STM1,
|
||||
),
|
||||
spool: StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
|
||||
tension_arm: TensionArm::new(AnalogInput::new(
|
||||
el7031_0030,
|
||||
EL7031_0030AnalogInputPort::AI1,
|
||||
)),
|
||||
laser: DigitalOutput::new(el2002, EL2002Port::DO1),
|
||||
namespace: Winder2Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
mode: mode.clone(),
|
||||
spool_step_converter: AngularStepConverter::new(200),
|
||||
spool_speed_controller: SpoolSpeedController::new(),
|
||||
last_measurement_emit: Instant::now(),
|
||||
spool_mode: mode.clone().into(),
|
||||
traverse_mode: mode.clone().into(),
|
||||
puller_mode: mode.into(),
|
||||
puller_speed_controller: PullerSpeedController::new(
|
||||
Velocity::new::<meter_per_minute>(1.0),
|
||||
Length::new::<millimeter>(1.75),
|
||||
LinearStepConverter::from_diameter(
|
||||
200, // Assuming 200 steps per revolution for the puller stepper,
|
||||
Length::new::<centimeter>(8.0), // 8cm diameter of the puller wheel
|
||||
),
|
||||
),
|
||||
traverse_controller: TraverseController::new(
|
||||
Length::new::<millimeter>(22.0), // Default inner limit
|
||||
Length::new::<millimeter>(92.0), // Default outer limit
|
||||
64, // Microsteps
|
||||
),
|
||||
emitted_default_state: false,
|
||||
spool_automatic_action: super::SpoolAutomaticAction {
|
||||
progress: Length::ZERO,
|
||||
progress_last_check: Instant::now(),
|
||||
target_length: Length::new::<meter>(250.0),
|
||||
mode: super::api::SpoolAutomaticActionMode::NoAction,
|
||||
},
|
||||
machine_identification_unique: machine_id,
|
||||
connected_machines: vec![],
|
||||
};
|
||||
|
||||
let channel = MachineChannel::new(machine_id);
|
||||
|
||||
let hardware = Hardware {
|
||||
spool_motor: StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
|
||||
traverse_motor: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
|
||||
traverse_limit_switch: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
|
||||
puller_motor: StepperVelocityEL70x1::new(el7031_0030.clone(), EL7031_0030StepperPort::STM1),
|
||||
tension_arm_sensor: AnalogInput::new(el7031_0030, EL7031_0030AnalogInputPort::AI1),
|
||||
traverse_laser_pointer: DigitalOutput::new(el2002, EL2002Port::DO1),
|
||||
};
|
||||
|
||||
let mut instance = Self::new(channel, hardware);
|
||||
|
||||
instance.emit_state();
|
||||
|
||||
Ok(instance)
|
||||
// initalize events
|
||||
new.emit_state();
|
||||
Ok(new)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
removed features:
|
||||
----------------------------------------------------------------------------------------------
|
||||
- removing puller target speed time series to accomodate different speed controllers
|
||||
|
||||
detected 'bugs':
|
||||
----------------------------------------------------------------------------------------------
|
||||
- SpoolLengthTask updated in pull mode??
|
||||
- Inverting direction of spool while running can damage motor
|
||||
- gear ratio inverted
|
||||
- Standby mode doesn't properly de-accelerate speed as it should for spool and puller
|
||||
- Hold mode doesn't de-accelerate Spool motor but puller motor
|
||||
- traverse position jumps into negative
|
||||
|
||||
adjustments:
|
||||
----------------------------------------------------------------------------------------------
|
||||
- Shift Gear speed change logic into backend from frontend
|
||||
|
||||
traverse:
|
||||
- finally dont allow to go in/out in UI
|
||||
-
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::{
|
||||
controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
|
||||
converters::linear_step_converter::LinearStepConverter,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use units::ConstZero;
|
||||
use units::acceleration::meter_per_minute_per_second;
|
||||
use units::f64::*;
|
||||
use units::jerk::meter_per_minute_per_second_squared;
|
||||
use units::velocity::meter_per_minute;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum GearRatio {
|
||||
OneToOne,
|
||||
OneToFive,
|
||||
OneToTen,
|
||||
}
|
||||
|
||||
impl GearRatio {
|
||||
/// Get the speed multiplier for this gear ratio
|
||||
pub fn multiplier(&self) -> f64 {
|
||||
match self {
|
||||
GearRatio::OneToOne => 1.0,
|
||||
GearRatio::OneToFive => 5.0,
|
||||
GearRatio::OneToTen => 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GearRatio {
|
||||
fn default() -> Self {
|
||||
GearRatio::OneToOne
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PullerSpeedController {
|
||||
enabled: bool,
|
||||
pub target_speed: Velocity,
|
||||
pub target_diameter: Length,
|
||||
pub regulation_mode: PullerRegulationMode,
|
||||
/// Forward rotation direction. If false, applies negative sign to speed
|
||||
pub forward: bool,
|
||||
/// Gear ratio for winding speed (1:5 or 1:10)
|
||||
pub gear_ratio: GearRatio,
|
||||
/// Linear acceleration controller to dampen speed change
|
||||
acceleration_controller: LinearJerkSpeedController,
|
||||
/// Converter for linear to angular transformations
|
||||
pub converter: LinearStepConverter,
|
||||
pub last_speed: Velocity,
|
||||
}
|
||||
|
||||
impl PullerSpeedController {
|
||||
pub fn new(
|
||||
target_speed: Velocity,
|
||||
target_diameter: Length,
|
||||
converter: LinearStepConverter,
|
||||
) -> Self {
|
||||
let acceleration = Acceleration::new::<meter_per_minute_per_second>(5.0);
|
||||
let jerk = Jerk::new::<meter_per_minute_per_second_squared>(10.0);
|
||||
let speed = Velocity::new::<meter_per_minute>(50.0);
|
||||
|
||||
Self {
|
||||
enabled: false,
|
||||
target_speed,
|
||||
target_diameter,
|
||||
regulation_mode: PullerRegulationMode::Speed,
|
||||
forward: true,
|
||||
gear_ratio: GearRatio::default(),
|
||||
acceleration_controller: LinearJerkSpeedController::new_simple(
|
||||
Some(speed),
|
||||
acceleration,
|
||||
jerk,
|
||||
),
|
||||
converter,
|
||||
last_speed: Velocity::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_target_speed(&mut self, target: Velocity) {
|
||||
self.target_speed = target;
|
||||
}
|
||||
|
||||
pub fn set_target_diameter(&mut self, target: Length) {
|
||||
self.target_diameter = target;
|
||||
}
|
||||
|
||||
pub const fn set_regulation_mode(&mut self, regulation: PullerRegulationMode) {
|
||||
self.regulation_mode = regulation;
|
||||
}
|
||||
|
||||
pub const fn set_forward(&mut self, forward: bool) {
|
||||
self.forward = forward;
|
||||
}
|
||||
|
||||
pub const fn set_gear_ratio(&mut self, gear_ratio: GearRatio) {
|
||||
self.gear_ratio = gear_ratio;
|
||||
}
|
||||
|
||||
pub const fn get_gear_ratio(&self) -> GearRatio {
|
||||
self.gear_ratio
|
||||
}
|
||||
|
||||
fn update_speed(&mut self, t: Instant) -> Velocity {
|
||||
let base_speed = match self.enabled {
|
||||
true => match self.regulation_mode {
|
||||
PullerRegulationMode::Speed => self.target_speed,
|
||||
PullerRegulationMode::Diameter => unimplemented!(),
|
||||
},
|
||||
false => Velocity::ZERO,
|
||||
};
|
||||
|
||||
// Apply gear ratio multiplier
|
||||
let speed = base_speed * self.gear_ratio.multiplier();
|
||||
|
||||
let speed = if self.forward { speed } else { -speed };
|
||||
|
||||
let speed = self.acceleration_controller.update(speed, t);
|
||||
|
||||
self.last_speed = speed;
|
||||
speed
|
||||
}
|
||||
|
||||
pub fn speed_to_angular_velocity(&self, speed: Velocity) -> AngularVelocity {
|
||||
// Use the converter to transform from linear velocity to angular velocity
|
||||
self.converter.velocity_to_angular_velocity(speed)
|
||||
}
|
||||
|
||||
pub fn angular_velocity_to_speed(&self, angular_speed: AngularVelocity) -> Velocity {
|
||||
// Use the converter to transform from angular velocity to linear velocity
|
||||
self.converter.angular_velocity_to_velocity(angular_speed)
|
||||
}
|
||||
|
||||
pub fn calc_angular_velocity(&mut self, t: Instant) -> AngularVelocity {
|
||||
let speed = self.update_speed(t);
|
||||
self.speed_to_angular_velocity(speed)
|
||||
}
|
||||
|
||||
pub fn get_target_speed(&self) -> Velocity {
|
||||
self.target_speed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub enum PullerRegulationMode {
|
||||
#[default]
|
||||
Speed,
|
||||
Diameter,
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use crate::winder2::{
|
||||
adaptive_spool_speed_controller::AdaptiveSpoolSpeedController,
|
||||
minmax_spool_speed_controller::MinMaxSpoolSpeedController,
|
||||
puller_speed_controller::PullerSpeedController,
|
||||
};
|
||||
use control_core::controllers::second_degree_motion::acceleration_position_controller::MotionControllerError;
|
||||
|
||||
use super::tension_arm::TensionArm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
use units::f64::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub enum SpoolSpeedControllerType {
|
||||
#[default]
|
||||
Adaptive,
|
||||
MinMax,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpoolSpeedController {
|
||||
adaptive_controller: AdaptiveSpoolSpeedController,
|
||||
minmax_controller: MinMaxSpoolSpeedController,
|
||||
r#type: SpoolSpeedControllerType,
|
||||
forward: bool,
|
||||
}
|
||||
|
||||
impl Default for SpoolSpeedController {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SpoolSpeedController {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
adaptive_controller: AdaptiveSpoolSpeedController::new(),
|
||||
minmax_controller: MinMaxSpoolSpeedController::new(),
|
||||
r#type: SpoolSpeedControllerType::Adaptive,
|
||||
forward: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_speed(&self) -> AngularVelocity {
|
||||
match self.r#type {
|
||||
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.get_speed(),
|
||||
SpoolSpeedControllerType::MinMax => self.minmax_controller.get_speed(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_speed(&mut self, speed: AngularVelocity) {
|
||||
match self.r#type {
|
||||
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.set_speed(speed),
|
||||
SpoolSpeedControllerType::MinMax => self.minmax_controller.set_speed(speed),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn set_enabled(&mut self, enabled: bool) {
|
||||
self.adaptive_controller.set_enabled(enabled);
|
||||
self.minmax_controller.set_enabled(enabled);
|
||||
}
|
||||
|
||||
pub const fn is_enabled(&self) -> bool {
|
||||
match self.r#type {
|
||||
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.is_enabled(),
|
||||
SpoolSpeedControllerType::MinMax => self.minmax_controller.is_enabled(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_type(&mut self, r#type: SpoolSpeedControllerType) {
|
||||
// If we're switching to a different type, copy the current speed to the target controller
|
||||
if std::mem::discriminant(&self.r#type) != std::mem::discriminant(&r#type) {
|
||||
// Get the current speed from the active controller
|
||||
let current_speed = match self.r#type {
|
||||
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.get_speed(),
|
||||
SpoolSpeedControllerType::MinMax => self.minmax_controller.get_speed(),
|
||||
};
|
||||
|
||||
// Set the speed in the target controller and reset it for smooth transition
|
||||
match r#type {
|
||||
SpoolSpeedControllerType::Adaptive => {
|
||||
self.adaptive_controller.set_speed(current_speed);
|
||||
self.adaptive_controller.reset();
|
||||
self.adaptive_controller.set_speed(current_speed); // Set again after reset to maintain speed
|
||||
}
|
||||
SpoolSpeedControllerType::MinMax => {
|
||||
self.minmax_controller.set_speed(current_speed);
|
||||
self.minmax_controller.reset();
|
||||
self.minmax_controller.set_speed(current_speed); // Set again after reset to maintain speed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.r#type = r#type;
|
||||
}
|
||||
|
||||
pub const fn get_type(&self) -> &SpoolSpeedControllerType {
|
||||
&self.r#type
|
||||
}
|
||||
|
||||
pub fn set_minmax_min_speed(
|
||||
&mut self,
|
||||
min_speed: AngularVelocity,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.minmax_controller.set_min_speed(min_speed)
|
||||
}
|
||||
|
||||
pub fn set_minmax_max_speed(
|
||||
&mut self,
|
||||
max_speed: AngularVelocity,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.minmax_controller.set_max_speed(max_speed)
|
||||
}
|
||||
|
||||
pub fn get_minmax_min_speed(&self) -> AngularVelocity {
|
||||
self.minmax_controller.get_min_speed()
|
||||
}
|
||||
|
||||
pub fn get_minmax_max_speed(&self) -> AngularVelocity {
|
||||
self.minmax_controller.get_max_speed()
|
||||
}
|
||||
|
||||
pub fn update_speed(
|
||||
&mut self,
|
||||
t: Instant,
|
||||
tension_arm: &TensionArm,
|
||||
puller_speed_controller: &PullerSpeedController,
|
||||
) -> AngularVelocity {
|
||||
match self.r#type {
|
||||
SpoolSpeedControllerType::Adaptive => {
|
||||
self.adaptive_controller
|
||||
.update_speed(t, tension_arm, puller_speed_controller)
|
||||
}
|
||||
SpoolSpeedControllerType::MinMax => self.minmax_controller.update_speed(t, tension_arm),
|
||||
}
|
||||
}
|
||||
|
||||
// Adaptive controller parameter getters and setters
|
||||
pub const fn get_adaptive_tension_target(&self) -> f64 {
|
||||
self.adaptive_controller.get_tension_target()
|
||||
}
|
||||
|
||||
pub const fn set_adaptive_tension_target(&mut self, tension_target: f64) {
|
||||
self.adaptive_controller.set_tension_target(tension_target);
|
||||
}
|
||||
|
||||
pub const fn get_adaptive_radius_learning_rate(&self) -> f64 {
|
||||
self.adaptive_controller.get_radius_learning_rate()
|
||||
}
|
||||
|
||||
pub const fn set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.adaptive_controller
|
||||
.set_radius_learning_rate(radius_learning_rate);
|
||||
}
|
||||
|
||||
pub const fn get_adaptive_max_speed_multiplier(&self) -> f64 {
|
||||
self.adaptive_controller.get_max_speed_multiplier()
|
||||
}
|
||||
|
||||
pub const fn set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.adaptive_controller
|
||||
.set_max_speed_multiplier(max_speed_multiplier);
|
||||
}
|
||||
|
||||
pub const fn get_adaptive_acceleration_factor(&self) -> f64 {
|
||||
self.adaptive_controller.get_acceleration_factor()
|
||||
}
|
||||
|
||||
pub const fn set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.adaptive_controller
|
||||
.set_acceleration_factor(acceleration_factor);
|
||||
}
|
||||
|
||||
pub const fn get_adaptive_deacceleration_urgency_multiplier(&self) -> f64 {
|
||||
self.adaptive_controller
|
||||
.get_deacceleration_urgency_multiplier()
|
||||
}
|
||||
|
||||
pub const fn set_adaptive_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.adaptive_controller
|
||||
.set_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
|
||||
}
|
||||
|
||||
pub const fn get_forward(&self) -> bool {
|
||||
self.forward
|
||||
}
|
||||
|
||||
pub const fn set_forward(&mut self, forward: bool) {
|
||||
self.forward = forward;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{ConstZero, Length, Velocity};
|
||||
use units::length::meter;
|
||||
use units::velocity::meter_per_second;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpoolLengthTask
|
||||
{
|
||||
current_length: Length,
|
||||
target_length: Length,
|
||||
last_check: Instant,
|
||||
}
|
||||
|
||||
impl SpoolLengthTask
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
SpoolLengthTask {
|
||||
target_length: Length::new::<meter>(0.0),
|
||||
current_length: Length::new::<meter>(0.0),
|
||||
last_check: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool
|
||||
{
|
||||
self.current_length >= self.target_length
|
||||
}
|
||||
|
||||
pub const fn current_length(&self) -> Length
|
||||
{
|
||||
self.current_length
|
||||
}
|
||||
|
||||
pub const fn target_length(&self) -> Length
|
||||
{
|
||||
self.target_length
|
||||
}
|
||||
|
||||
pub fn set_target_length(&mut self, target_length: Length)
|
||||
{
|
||||
self.target_length = target_length;
|
||||
}
|
||||
|
||||
pub fn update_timer(&mut self, now: Instant)
|
||||
{
|
||||
self.last_check = now;
|
||||
}
|
||||
|
||||
pub fn update_progress(&mut self, now: Instant, velocity: Velocity)
|
||||
{
|
||||
// Calculate time elapsed since last progress check (in minutes)
|
||||
let dt = now.duration_since(self.last_check).as_secs_f64();
|
||||
|
||||
// Calculate distance pulled during this time interval
|
||||
let meters_pulled_this_interval = Length::new::<meter>(
|
||||
velocity.get::<meter_per_second>() * dt,
|
||||
);
|
||||
|
||||
// Update total meters pulled
|
||||
self.current_length += meters_pulled_this_interval.abs();
|
||||
self.last_check = now;
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, now: Instant)
|
||||
{
|
||||
self.current_length = Length::ZERO;
|
||||
self.last_check = now;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,69 @@
|
||||
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
|
||||
use units::angle::revolution;
|
||||
use units::electric_potential::volt;
|
||||
use units::f64::*;
|
||||
|
||||
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TensionArm
|
||||
{
|
||||
sensor: AnalogInput,
|
||||
zero_offset: Angle,
|
||||
is_calibrated: bool,
|
||||
pub struct TensionArm {
|
||||
pub analog_input: AnalogInput,
|
||||
pub zero: Angle,
|
||||
/// was zeroed at least once
|
||||
pub zeroed: bool,
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl TensionArm
|
||||
{
|
||||
pub fn new(sensor: AnalogInput) -> Self
|
||||
{
|
||||
impl TensionArm {
|
||||
pub fn new(analog_input: AnalogInput) -> Self {
|
||||
Self {
|
||||
sensor,
|
||||
zero_offset: Angle::new::<revolution>(0.0),
|
||||
is_calibrated: false,
|
||||
analog_input,
|
||||
zero: Angle::new::<revolution>(0.0),
|
||||
zeroed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calibrate(&mut self)
|
||||
{
|
||||
self.zero_offset = self.raw_angle();
|
||||
self.is_calibrated = true;
|
||||
}
|
||||
|
||||
pub const fn is_calibrated(&self) -> bool
|
||||
{
|
||||
self.is_calibrated
|
||||
}
|
||||
|
||||
pub fn get_angle(&self) -> Angle
|
||||
{
|
||||
// revolution is maping -1/1 to 0/1
|
||||
let raw = self.raw_angle();
|
||||
|
||||
// Handle the wraparound case
|
||||
if raw < self.zero_offset {
|
||||
// We've wrapped around, so add a full revolution
|
||||
(raw + Angle::new::<revolution>(1.0)) - self.zero_offset
|
||||
} else {
|
||||
// Normal case
|
||||
raw - self.zero_offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
impl TensionArm
|
||||
{
|
||||
fn volts_to_angle(&self, volts: f64) -> Angle
|
||||
{
|
||||
// 0V = 0deg -> 5V = 3600deg
|
||||
fn volts_to_angle(&self, volts: f64) -> Angle {
|
||||
// 0V = 0deg 5V = 3600deg
|
||||
// always wrap into 0..1 revolution
|
||||
Angle::new::<revolution>(volts / 5.0) % Angle::new::<revolution>(1.0)
|
||||
}
|
||||
|
||||
fn get_volts(&self) -> f64
|
||||
{
|
||||
// TODO: reconsider panicking, to allow system to shutdown gracefully.
|
||||
// but since I am only moving code, I will not touch this...
|
||||
let AnalogInputValue::Potential(potential) =
|
||||
self.sensor.get_physical() else { panic!("Expected a potential value"); };
|
||||
fn get_volts(&self) -> f64 {
|
||||
// get the normalized value from the analog input
|
||||
let value = self.analog_input.get_physical();
|
||||
|
||||
potential.get::<volt>()
|
||||
match value {
|
||||
AnalogInputValue::Potential(v) => v.get::<volt>(),
|
||||
_ => panic!("Expected a potential value"),
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_angle(&self) -> Angle
|
||||
{
|
||||
self.volts_to_angle(self.get_volts())
|
||||
fn raw_angle(&self) -> Angle {
|
||||
// get volts
|
||||
let volts = self.get_volts();
|
||||
|
||||
// 0V = 0deg 5V = 3600deg
|
||||
self.volts_to_angle(volts)
|
||||
}
|
||||
|
||||
pub fn get_angle(&self) -> Angle {
|
||||
// revolution is maping -1/1 to 0/1
|
||||
let raw = self.raw_angle();
|
||||
|
||||
// Handle the wraparound case
|
||||
if raw < self.zero {
|
||||
// We've wrapped around, so add a full revolution
|
||||
(raw + Angle::new::<revolution>(1.0)) - self.zero
|
||||
} else {
|
||||
// Normal case
|
||||
raw - self.zero
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero(&mut self) {
|
||||
self.zero = self.raw_angle();
|
||||
self.zeroed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -87,6 +73,7 @@ mod tests {
|
||||
analog_input::{AnalogInputInput, physical::AnalogInputRange},
|
||||
analog_input_dummy::AnalogInputDummy,
|
||||
};
|
||||
use std::i16;
|
||||
|
||||
#[test]
|
||||
fn volts_to_angle() {
|
||||
@@ -143,7 +130,7 @@ mod tests {
|
||||
normalized: (5.0 / 10.0),
|
||||
wiring_error: false,
|
||||
});
|
||||
let physical = tension_arm.sensor.get_physical();
|
||||
let physical = tension_arm.analog_input.get_physical();
|
||||
match physical {
|
||||
AnalogInputValue::Potential(v) => {
|
||||
assert_relative_eq!(v.get::<volt>(), 5.0, epsilon = f64::EPSILON);
|
||||
@@ -0,0 +1,517 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::converters::linear_step_converter::LinearStepConverter;
|
||||
use ethercat_hal::io::{
|
||||
digital_input::DigitalInput, stepper_velocity_el70x1::StepperVelocityEL70x1,
|
||||
};
|
||||
use units::ConstZero;
|
||||
use units::angular_velocity::revolution_per_second;
|
||||
use units::f64::{AngularVelocity, Length, Velocity};
|
||||
use units::length::millimeter;
|
||||
use units::velocity::millimeter_per_second;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TraverseController {
|
||||
enabled: bool,
|
||||
position: Length,
|
||||
limit_inner: Length,
|
||||
limit_outer: Length,
|
||||
step_size: Length,
|
||||
padding: Length,
|
||||
state: State,
|
||||
fullstep_converter: LinearStepConverter,
|
||||
microstep_converter: LinearStepConverter,
|
||||
// A sticky flag if the [`State`] changed (not the sub states)
|
||||
// Needed to send state updates to the UI
|
||||
did_change_state: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum State {
|
||||
/// Initial state
|
||||
NotHomed,
|
||||
|
||||
/// Doing nothing
|
||||
/// Already homed
|
||||
Idle,
|
||||
|
||||
/// Going to inner limit
|
||||
///
|
||||
/// After reaching the inner limit, the state will change to [`State::Idle`]
|
||||
GoingIn,
|
||||
|
||||
/// Going to outer limit
|
||||
///
|
||||
/// After reaching the outer limit, the state will change to [`State::Idle`]
|
||||
GoingOut,
|
||||
|
||||
/// Homing is in progress
|
||||
///
|
||||
/// After homing is done, the state will change to [`State::Idle`]
|
||||
Homing(HomingState),
|
||||
|
||||
/// Move between inner and outer limits
|
||||
Traversing(TraversingState),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum TraversingState {
|
||||
/// Like [`State::GoingOut`] but
|
||||
/// - will go into [`State::GoingIn`] after reaching the outer limit
|
||||
GoingOut,
|
||||
|
||||
/// Like [`State::GoingIn`] but
|
||||
/// - will go into [`State::GoingOut`] after reaching the inner limit
|
||||
/// - speed is synced to spool speed
|
||||
TraversingIn,
|
||||
|
||||
/// Like [`State::GoingOut`] but
|
||||
/// - will go into [`State::GoingIn`] after reaching the outer limit
|
||||
/// - speed is synced to spool speed
|
||||
TraversingOut,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum HomingState {
|
||||
/// In this state the traverse is not moving but checks if the endstop si triggered
|
||||
/// If the endstop is triggered we go into [`HomingState::EscapeEndstop`]
|
||||
/// If the endstop is not triggered we go into [`HomingState::FindEndstop`]
|
||||
Initialize,
|
||||
|
||||
/// In this state the traverse is moving out away from the endstop until it's not triggered anymore
|
||||
/// The it goes into [`HomingState::FindEnstopFineDistancing`]
|
||||
EscapeEndstop,
|
||||
|
||||
/// Moving out away from the endstop
|
||||
/// Then Transition into [`HomingState::FindEndtopFine`]
|
||||
FindEndstopFineDistancing,
|
||||
|
||||
/// In this state the traverse is fast until it reaches the endstop
|
||||
FindEndstopCoarse,
|
||||
|
||||
/// In this state the traverse is moving slowly until it reaches the endstop
|
||||
FindEndtopFine,
|
||||
|
||||
/// In this state we check if th current position is actually 0.0, if not we redo the homing routine
|
||||
Validate(Instant),
|
||||
}
|
||||
|
||||
impl TraverseController {
|
||||
pub fn new(limit_inner: Length, limit_outer: Length, microsteps: u8) -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
position: Length::ZERO,
|
||||
limit_inner,
|
||||
limit_outer,
|
||||
step_size: Length::new::<millimeter>(1.75), // Default step size
|
||||
padding: Length::new::<millimeter>(0.88), // Default padding
|
||||
state: State::NotHomed,
|
||||
did_change_state: false,
|
||||
fullstep_converter: LinearStepConverter::from_circumference(
|
||||
200,
|
||||
Length::new::<millimeter>(35.0),
|
||||
),
|
||||
microstep_converter: LinearStepConverter::from_circumference(
|
||||
200 * microsteps as i16,
|
||||
Length::new::<millimeter>(35.0),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getter & Setter
|
||||
impl TraverseController {
|
||||
pub const fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_limit_inner(&mut self, limit: Length) {
|
||||
self.limit_inner = limit;
|
||||
}
|
||||
|
||||
pub fn set_limit_outer(&mut self, limit: Length) {
|
||||
self.limit_outer = limit;
|
||||
}
|
||||
|
||||
pub fn set_step_size(&mut self, step_size: Length) {
|
||||
self.step_size = step_size;
|
||||
}
|
||||
|
||||
pub fn set_padding(&mut self, padding: Length) {
|
||||
self.padding = padding;
|
||||
}
|
||||
|
||||
pub fn get_limit_inner(&self) -> Length {
|
||||
self.limit_inner
|
||||
}
|
||||
|
||||
pub fn get_limit_outer(&self) -> Length {
|
||||
self.limit_outer
|
||||
}
|
||||
|
||||
pub fn get_step_size(&self) -> Length {
|
||||
self.step_size
|
||||
}
|
||||
|
||||
pub fn get_padding(&self) -> Length {
|
||||
self.padding
|
||||
}
|
||||
|
||||
pub fn get_current_position(&self) -> Option<Length> {
|
||||
match self.is_homed() {
|
||||
true => Some(self.position),
|
||||
false => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
pub const fn did_change_state(&mut self) -> bool {
|
||||
let did_change = self.did_change_state;
|
||||
// Reset the flag
|
||||
self.did_change_state = false;
|
||||
did_change
|
||||
}
|
||||
}
|
||||
|
||||
// State management
|
||||
impl TraverseController {
|
||||
pub const fn goto_limit_inner(&mut self) {
|
||||
self.state = State::GoingIn;
|
||||
}
|
||||
|
||||
pub const fn goto_limit_outer(&mut self) {
|
||||
self.state = State::GoingOut;
|
||||
}
|
||||
|
||||
pub const fn goto_home(&mut self) {
|
||||
self.state = State::Homing(HomingState::Initialize);
|
||||
}
|
||||
|
||||
pub const fn start_traversing(&mut self) {
|
||||
self.state = State::Traversing(TraversingState::GoingOut);
|
||||
}
|
||||
|
||||
pub const fn is_homed(&self) -> bool {
|
||||
// if not [`State::NotHomed`], then it is homed
|
||||
!matches!(self.state, State::NotHomed)
|
||||
}
|
||||
|
||||
pub const fn is_going_in(&self) -> bool {
|
||||
// [`State::GoingIn`]
|
||||
matches!(self.state, State::GoingIn)
|
||||
}
|
||||
|
||||
pub const fn is_going_out(&self) -> bool {
|
||||
// [`State::GoingOut`]
|
||||
matches!(self.state, State::GoingOut)
|
||||
}
|
||||
|
||||
pub const fn is_going_home(&self) -> bool {
|
||||
// [`State::Homing`]
|
||||
matches!(self.state, State::Homing(_))
|
||||
}
|
||||
|
||||
pub const fn is_traversing(&self) -> bool {
|
||||
// [`State::Traversing`]
|
||||
matches!(self.state, State::Traversing(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl TraverseController {
|
||||
// If at inner limit within a tolerance
|
||||
fn is_at_position(&self, target_position: Length, tolerance: Length) -> bool {
|
||||
let upper_tolerance = target_position + tolerance.abs();
|
||||
let lower_tolerance = target_position - tolerance.abs();
|
||||
self.position >= lower_tolerance && self.position <= upper_tolerance
|
||||
}
|
||||
|
||||
/// Calculate distance to position
|
||||
fn distance_to_position(&self, target_position: Length) -> Length {
|
||||
if self.position > target_position {
|
||||
self.position - target_position
|
||||
} else if self.position < target_position {
|
||||
target_position - self.position
|
||||
} else {
|
||||
Length::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
// Changes the direction of the speed based on the current position and target position
|
||||
fn speed_to_position(&self, target_position: Length, absolute_speed: Velocity) -> Velocity {
|
||||
// If we are over the target position we need to move negative
|
||||
if self.position > target_position {
|
||||
-absolute_speed.abs()
|
||||
} else if self.position < target_position {
|
||||
absolute_speed.abs()
|
||||
} else {
|
||||
Velocity::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current traverse position as a [`Length`].
|
||||
pub fn sync_position(&mut self, traverse: &StepperVelocityEL70x1) {
|
||||
let steps = traverse.get_position();
|
||||
self.position = self.microstep_converter.steps_to_distance(steps as f64);
|
||||
}
|
||||
|
||||
/// Update the [`did_change_state`] flag
|
||||
/// Only considers the major state not the sub states
|
||||
const fn update_did_change_state(&mut self, old_state: &State) -> bool {
|
||||
match self.state {
|
||||
State::NotHomed => !matches!(old_state, State::NotHomed),
|
||||
State::Idle => !matches!(old_state, State::Idle),
|
||||
State::GoingIn => !matches!(old_state, State::GoingIn),
|
||||
State::GoingOut => !matches!(old_state, State::GoingOut),
|
||||
State::Homing(_) => !matches!(old_state, State::Homing(_)),
|
||||
State::Traversing(_) => !matches!(old_state, State::Traversing(_)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates a desired speed based on the current state and the end stop status.
|
||||
///
|
||||
/// Positive speed moved out, negative speed moves in.
|
||||
fn get_speed(
|
||||
&mut self,
|
||||
traverse: &mut StepperVelocityEL70x1,
|
||||
traverse_end_stop: &DigitalInput,
|
||||
spool_speed: AngularVelocity,
|
||||
) -> Velocity {
|
||||
// Don't move if not enabled or in a state that doesn't result in movement
|
||||
if !self.enabled {
|
||||
return Velocity::ZERO;
|
||||
}
|
||||
|
||||
self.sync_position(traverse);
|
||||
|
||||
// save state before
|
||||
let old_state = self.state.clone();
|
||||
|
||||
// Automatic Transitions
|
||||
match &self.state {
|
||||
State::NotHomed => {}
|
||||
State::Idle => {}
|
||||
State::GoingIn => {
|
||||
// If inner limit is reached
|
||||
if self.is_at_position(self.limit_inner, Length::new::<millimeter>(0.01)) {
|
||||
// Put Into Idle
|
||||
self.state = State::Idle;
|
||||
}
|
||||
}
|
||||
State::GoingOut => {
|
||||
// If outer limit is reached
|
||||
if self.is_at_position(self.limit_outer, Length::new::<millimeter>(0.01)) {
|
||||
// Put Into Idle
|
||||
self.state = State::Idle;
|
||||
}
|
||||
}
|
||||
State::Homing(homing_state) => match homing_state {
|
||||
HomingState::Initialize => {
|
||||
// If endstop is triggered, escape the endstop
|
||||
if traverse_end_stop.get_value().unwrap_or(false) {
|
||||
self.state = State::Homing(HomingState::EscapeEndstop);
|
||||
} else {
|
||||
// If endstop is not triggered, move to the endstop
|
||||
self.state = State::Homing(HomingState::FindEndstopCoarse);
|
||||
}
|
||||
}
|
||||
HomingState::EscapeEndstop => {
|
||||
// Move out until endstop is not triggered anymore
|
||||
if !traverse_end_stop.get_value().unwrap_or(false) {
|
||||
self.state = State::Homing(HomingState::FindEndstopFineDistancing);
|
||||
}
|
||||
}
|
||||
HomingState::FindEndstopFineDistancing => {
|
||||
// Move out until endstop is not triggered anymore
|
||||
if !traverse_end_stop.get_value().unwrap_or(false) {
|
||||
// Find endstop fine
|
||||
self.state = State::Homing(HomingState::FindEndtopFine);
|
||||
}
|
||||
}
|
||||
HomingState::FindEndtopFine => {
|
||||
// If endstop is reached change to idle
|
||||
if traverse_end_stop.get_value().unwrap_or(false) {
|
||||
// Set poition of traverse to 0
|
||||
traverse.set_position(0);
|
||||
// Put Into Idle
|
||||
self.state = State::Homing(HomingState::Validate(Instant::now()));
|
||||
}
|
||||
}
|
||||
HomingState::FindEndstopCoarse => {
|
||||
// Move to endstop
|
||||
if traverse_end_stop.get_value().unwrap_or(false) {
|
||||
// Move awaiy from endstop
|
||||
self.state = State::Homing(HomingState::FindEndstopFineDistancing);
|
||||
}
|
||||
}
|
||||
HomingState::Validate(instant) => {
|
||||
// If 100ms have passed check if position is actually 0.0
|
||||
if instant.elapsed().as_millis() > 100 {
|
||||
if self.is_at_position(Length::ZERO, Length::new::<millimeter>(0.01)) {
|
||||
// If position is 0.0, put into idle
|
||||
self.state = State::Idle;
|
||||
} else {
|
||||
// If position is not 0.0, redo homing
|
||||
self.state = State::Homing(HomingState::Initialize);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// If state changed we
|
||||
State::Traversing(traversing_state) => match traversing_state {
|
||||
TraversingState::GoingOut => {
|
||||
// If outer limit is reached
|
||||
if self.position >= self.limit_outer - self.padding {
|
||||
// Turn around
|
||||
self.state = State::Traversing(TraversingState::TraversingIn);
|
||||
}
|
||||
}
|
||||
TraversingState::TraversingIn => {
|
||||
// If inner limit is reached
|
||||
if self.position <= self.limit_inner + self.padding {
|
||||
// Turn around
|
||||
self.state = State::Traversing(TraversingState::TraversingOut);
|
||||
}
|
||||
}
|
||||
TraversingState::TraversingOut => {
|
||||
// If outer limit is reached
|
||||
if self.position >= self.limit_outer - self.padding {
|
||||
// Turn around
|
||||
self.state = State::Traversing(TraversingState::TraversingIn);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Set the [`did_change_state`] flag
|
||||
if !self.did_change_state {
|
||||
self.did_change_state = self.update_did_change_state(&old_state);
|
||||
}
|
||||
|
||||
// Speed
|
||||
|
||||
match &self.state {
|
||||
State::NotHomed => Velocity::ZERO, // Not homed, no movement
|
||||
State::Idle => Velocity::ZERO, // No movement in idle state
|
||||
State::GoingIn => {
|
||||
// Move in at a speed of 10-100 mm/s
|
||||
self.speed_to_position(
|
||||
self.limit_inner,
|
||||
match self.distance_to_position(self.limit_inner).abs()
|
||||
> Length::new::<millimeter>(1.0)
|
||||
{
|
||||
true => Velocity::new::<millimeter_per_second>(100.0),
|
||||
false => Velocity::new::<millimeter_per_second>(10.0),
|
||||
},
|
||||
)
|
||||
}
|
||||
State::GoingOut => {
|
||||
// Move out at a speed of 10-100 mm/s
|
||||
self.speed_to_position(
|
||||
self.limit_outer,
|
||||
match self.distance_to_position(self.limit_outer).abs()
|
||||
> Length::new::<millimeter>(1.0)
|
||||
{
|
||||
true => Velocity::new::<millimeter_per_second>(100.0),
|
||||
false => Velocity::new::<millimeter_per_second>(10.0),
|
||||
},
|
||||
)
|
||||
}
|
||||
State::Homing(homing_state) => match homing_state {
|
||||
HomingState::Initialize => Velocity::ZERO,
|
||||
HomingState::EscapeEndstop => {
|
||||
// Move out at a speed of 10 mm/s
|
||||
Velocity::new::<millimeter_per_second>(10.0)
|
||||
}
|
||||
HomingState::FindEndstopFineDistancing => {
|
||||
// Move out at a speed of 2 mm/s
|
||||
Velocity::new::<millimeter_per_second>(2.0)
|
||||
}
|
||||
HomingState::FindEndstopCoarse => {
|
||||
// Move in at a speed of -100 mm/s
|
||||
Velocity::new::<millimeter_per_second>(-100.0)
|
||||
}
|
||||
HomingState::FindEndtopFine => {
|
||||
// move into the endstop at 2 mm/s
|
||||
Velocity::new::<millimeter_per_second>(-2.0)
|
||||
}
|
||||
HomingState::Validate(_) => {
|
||||
// We stand still until the validation cooldown has passed
|
||||
Velocity::ZERO
|
||||
}
|
||||
}, // Homing speed
|
||||
State::Traversing(traversing_state) => match traversing_state {
|
||||
TraversingState::GoingOut => {
|
||||
// Move out at a speed of 100 mm/s
|
||||
self.speed_to_position(
|
||||
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
|
||||
Velocity::new::<millimeter_per_second>(100.0),
|
||||
)
|
||||
}
|
||||
TraversingState::TraversingIn => self.speed_to_position(
|
||||
self.limit_inner + self.padding - Length::new::<millimeter>(0.01),
|
||||
Self::calculate_traverse_speed(spool_speed, self.step_size),
|
||||
),
|
||||
TraversingState::TraversingOut => self.speed_to_position(
|
||||
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
|
||||
Self::calculate_traverse_speed(spool_speed, self.step_size),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the traverse speed
|
||||
///
|
||||
/// The traverse speed is the linear speed at which the winding mechanism moves along the spool.
|
||||
/// It's directly proportional to how fast the spool rotates and how far the traverse moves per rotation.
|
||||
///
|
||||
/// - Traverse Distance per Revolution [mm] = Step Size [mm]
|
||||
/// - Traverse Speed [mm/s] = Spool Speed [rev/s or rad/s] * Step Size [mm]
|
||||
///
|
||||
/// Note: While the traverse range (from outer limit minus padding to inner limit plus padding)
|
||||
/// determines the total area to be covered, the traverse speed itself depends only on
|
||||
/// the step size and spool rotation speed.
|
||||
pub fn calculate_traverse_speed(spool_speed: AngularVelocity, step_size: Length) -> Velocity {
|
||||
// Calculate the traverse speed directly from spool speed and step size
|
||||
let traverse_speed: Velocity = Velocity::new::<millimeter_per_second>(
|
||||
spool_speed.get::<revolution_per_second>() * step_size.get::<millimeter>(),
|
||||
);
|
||||
|
||||
traverse_speed
|
||||
}
|
||||
|
||||
pub fn update_speed(
|
||||
&mut self,
|
||||
traverse: &mut StepperVelocityEL70x1,
|
||||
traverse_end_stop: &DigitalInput,
|
||||
spool_speed: AngularVelocity,
|
||||
) {
|
||||
let speed = self.get_speed(traverse, traverse_end_stop, spool_speed);
|
||||
let steps_per_second = self.fullstep_converter.velocity_to_steps(speed);
|
||||
// ignore if we can't set speed
|
||||
let _ = traverse.set_speed(steps_per_second);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_calculate_traverse_speed() {
|
||||
let spool_speed = AngularVelocity::new::<revolution_per_second>(1.0); // 1 revolution per second
|
||||
let step_size = Length::new::<millimeter>(1.75); // 1.75 mm step size
|
||||
|
||||
let traverse_speed = TraverseController::calculate_traverse_speed(spool_speed, step_size);
|
||||
assert_relative_eq!(
|
||||
traverse_speed.get::<millimeter_per_second>(),
|
||||
1.75,
|
||||
epsilon = f64::EPSILON
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use ethercat_hal::io::{
|
||||
analog_input::AnalogInput,
|
||||
digital_input::DigitalInput,
|
||||
digital_output::DigitalOutput,
|
||||
stepper_velocity_el70x1::StepperVelocityEL70x1
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Mode
|
||||
{
|
||||
Standby,
|
||||
Hold,
|
||||
Pull,
|
||||
Wind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SpoolLengthTaskCompletedAction
|
||||
{
|
||||
NoAction,
|
||||
Pull,
|
||||
Hold,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Hardware
|
||||
{
|
||||
pub spool_motor: StepperVelocityEL70x1,
|
||||
pub traverse_motor: StepperVelocityEL70x1,
|
||||
pub traverse_limit_switch: DigitalInput,
|
||||
pub traverse_laser_pointer: DigitalOutput,
|
||||
pub puller_motor: StepperVelocityEL70x1,
|
||||
pub tension_arm_sensor: AnalogInput,
|
||||
}
|
||||
+16
-10
@@ -15,7 +15,7 @@ in {
|
||||
consoleMode = "max"; # Use the highest available resolution
|
||||
};
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_6_13;
|
||||
boot.kernelPackages = pkgs.linuxPackages_6_18;
|
||||
boot.kernelModules = [ "i915" ];
|
||||
|
||||
boot.kernelParams = [
|
||||
@@ -134,7 +134,6 @@ in {
|
||||
|
||||
# Enable the X11 windowing system.
|
||||
services.xserver.enable = true;
|
||||
#services.xserver.videoDrivers = [ "intel" ];
|
||||
services.xserver.displayManager.gdm = {
|
||||
enable = true;
|
||||
autoSuspend = false;
|
||||
@@ -186,18 +185,25 @@ in {
|
||||
powertop.enable = false;
|
||||
};
|
||||
|
||||
# Ensure all power management is disabled
|
||||
|
||||
services.logind = {
|
||||
# This remains for backward compatibility/high-level override
|
||||
lidSwitch = "ignore";
|
||||
extraConfig = ''
|
||||
HandlePowerKey=ignore
|
||||
HandleSuspendKey=ignore
|
||||
HandleHibernateKey=ignore
|
||||
HandleLidSwitch=ignore
|
||||
IdleAction=ignore
|
||||
'';
|
||||
|
||||
# Structured settings for logind.conf
|
||||
settings = {
|
||||
Login = {
|
||||
HandlePowerKey = "ignore";
|
||||
HandleSuspendKey = "ignore";
|
||||
HandleHibernateKey = "ignore";
|
||||
HandleLidSwitch = "ignore";
|
||||
IdleAction = "ignore";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
# Configure keymap in X11
|
||||
services.xserver.xkb = {
|
||||
layout = "de";
|
||||
|
||||
+2
-13
@@ -3,19 +3,8 @@
|
||||
|
||||
home.packages = with pkgs; [ pkgs.qitechPackages.electron ];
|
||||
|
||||
# Add autostart entry for the QiTech electron app
|
||||
xdg.configFile."autostart/de.qitech.control-electron.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=QiTech Control
|
||||
Comment=QiTech Control
|
||||
Exec=qitech-control-electron
|
||||
Icon=de.qitech.control-electron
|
||||
Terminal=false
|
||||
StartupWMClass=QiTech Control
|
||||
X-GNOME-Autostart-enabled=true
|
||||
X-GNOME-Autostart-Phase=Applications
|
||||
'';
|
||||
xdg.configFile."autostart/de.qitech.control-electron.desktop".source =
|
||||
"${pkgs.qitechPackages.electron}/share/applications/de.qitech.control-electron.desktop";
|
||||
|
||||
dconf.settings = {
|
||||
# Set GNOME wallpaper
|
||||
|
||||
@@ -10,7 +10,7 @@ buildNpmPackage rec {
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
|
||||
|
||||
makeCacheWritable = true;
|
||||
npmDepsHash = "sha256-N2kdS9IKkEMNOPPnRNQ9GvCenR/k88fVyAFzXMJyxs4=";
|
||||
npmDepsHash = "sha256-jW9nogboiuYBzjLNM5rlYsdfp+58OlDf+kxjgNlST70=";
|
||||
npmFlags = [ "--no-audit" "--no-fund" ];
|
||||
|
||||
installPhase = ''
|
||||
@@ -44,7 +44,6 @@ buildNpmPackage rec {
|
||||
--enable-gpu \
|
||||
--ignore-gpu-blocklist \
|
||||
--enable-gpu-rasterization \
|
||||
--disable-software-rasterizer \
|
||||
"$@"
|
||||
EOF
|
||||
chmod +x $out/bin/qitech-control-electron
|
||||
|
||||
@@ -30,6 +30,9 @@ docker run --rm \
|
||||
--workdir /workspace \
|
||||
nixos/nix:latest \
|
||||
sh -c "
|
||||
whoami
|
||||
ls -lh
|
||||
cd /workspace
|
||||
echo 'Enabling flakes and nix-command...'
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' > ~/.config/nix/nix.conf
|
||||
|
||||
Generated
+868
@@ -0,0 +1,868 @@
|
||||
{
|
||||
"name": "control",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"electron": "^38.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"got": "^11.8.5",
|
||||
"progress": "^2.0.3",
|
||||
"semver": "^6.2.0",
|
||||
"sumchecker": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"global-agent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
"@types/keyv": "^3.1.4",
|
||||
"@types/node": "*",
|
||||
"@types/responselike": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
||||
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^4.0.0",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"normalize-url": "^6.0.1",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "38.8.4",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-38.8.4.tgz",
|
||||
"integrity": "sha512-vYZ2SH2OhzkCmkbRaijtZLIEJRMnLUL2KQmcAM/7kamf4++3AKSNCBjE1epmQnYFY1M4oKjOUcS3G02ayxR5Bw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"electron": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20.55"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/globalthis": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-properties": "^1.2.1",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "11.8.6",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"electron": "^38.8.4"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -46,7 +46,7 @@ tracing-subscriber = { version = "0.3.22", features = [
|
||||
utils = { version = "0.1.0", path="../utils" }
|
||||
spin_sleep = "1.3.3"
|
||||
dhat = { version = "0.3.3", optional = true }
|
||||
ctrlc = { version = "3.3", optional = true }
|
||||
ctrlc = { version = "3.5", optional = true }
|
||||
ta = "0.5.0"
|
||||
|
||||
|
||||
|
||||
+2
-56
@@ -3,8 +3,6 @@ use crate::{
|
||||
socketio::main_namespace::machines_event::MachineObj,
|
||||
};
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use control_core::socketio::event::GenericEvent;
|
||||
|
||||
use machines::{
|
||||
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial,
|
||||
MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
|
||||
@@ -15,8 +13,8 @@ use machines::{
|
||||
},
|
||||
registry::{MACHINE_REGISTRY, MachineRegistry},
|
||||
serial::{devices::laser::Laser, init::SerialDetection},
|
||||
winder2::api::GenericEvent,
|
||||
};
|
||||
|
||||
use socketio::{
|
||||
main_namespace::{MainNamespaceEvents, ethercat_devices_event::EthercatDevicesEventBuilder},
|
||||
queue::start_socketio_queue,
|
||||
@@ -35,7 +33,6 @@ use rest::init::start_api_thread;
|
||||
use serialport::UsbPortInfo;
|
||||
use smol::{
|
||||
channel::{Receiver, Sender},
|
||||
future,
|
||||
lock::RwLock,
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
@@ -258,7 +255,6 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
|
||||
while let Ok(message) = recv.recv().await {
|
||||
match message {
|
||||
AsyncThreadMessage::NoMsg => (),
|
||||
|
||||
AsyncThreadMessage::ConnectOneWayRequest(cross_connection) => {
|
||||
let api_machines_guard = shared_state.api_machines.lock().await;
|
||||
// The Src Connection is from the machine that recvs the request to connect
|
||||
@@ -270,8 +266,6 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
|
||||
None => continue,
|
||||
};
|
||||
|
||||
tracing::warn!("Received signal for ONE WAY REQUEST");
|
||||
|
||||
let dest_sender = match api_machines_guard.get(&dest_ident) {
|
||||
Some(sender) => sender,
|
||||
None => continue,
|
||||
@@ -288,54 +282,6 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
|
||||
Err(_) => tracing::error!("Failed to send MachineConnection"),
|
||||
}
|
||||
}
|
||||
AsyncThreadMessage::ConnectTwoWayRequest(cross_connection) => {
|
||||
let api_machines_guard = shared_state.api_machines.lock().await;
|
||||
|
||||
let src_ident = cross_connection.src;
|
||||
let dest_ident = cross_connection.dest;
|
||||
|
||||
let src_sender = match api_machines_guard.get(&src_ident) {
|
||||
Some(sender) => sender,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let dest_sender = match api_machines_guard.get(&dest_ident) {
|
||||
Some(sender) => sender,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// connect src -> dest
|
||||
let connection_src_to_dest = MachineConnection {
|
||||
ident: dest_ident.clone(),
|
||||
connection: dest_sender.clone(),
|
||||
};
|
||||
|
||||
let res = src_sender
|
||||
.send(machines::MachineMessage::ConnectToMachine(
|
||||
connection_src_to_dest,
|
||||
))
|
||||
.await;
|
||||
match res {
|
||||
Ok(_) => (),
|
||||
Err(_) => tracing::error!("Failed to send MachineConnection src -> dest"),
|
||||
}
|
||||
|
||||
// connect dest -> src
|
||||
let connection_dest_to_src = MachineConnection {
|
||||
ident: src_ident.clone(),
|
||||
connection: src_sender.clone(),
|
||||
};
|
||||
|
||||
let res = dest_sender
|
||||
.send(machines::MachineMessage::ConnectToMachine(
|
||||
connection_dest_to_src,
|
||||
))
|
||||
.await;
|
||||
match res {
|
||||
Ok(_) => (),
|
||||
Err(_) => tracing::error!("Failed to send MachineConnection dest -> src"),
|
||||
}
|
||||
}
|
||||
AsyncThreadMessage::DisconnectMachines(cross_connection) => {
|
||||
let api_machines_guard = shared_state.api_machines.lock().await;
|
||||
// The Src Connection is from the machine that recvs the request to connect
|
||||
@@ -466,7 +412,7 @@ fn main() {
|
||||
app_state.clone(),
|
||||
));
|
||||
}
|
||||
future::yield_now().await;
|
||||
smol::Timer::after(Duration::from_micros(150)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user