Compare commits

...
Author SHA1 Message Date
github-actions[bot] 08c62fac48 chore: update npmDepsHash to sha256-izbpayf659KJUAfdNwQ8rKBULNZLzsgTWSs1sGOj00A= 2026-02-11 14:43:11 +00:00
snacj d6ef555378 Fixed Formatting 2026-02-11 15:40:43 +01:00
Snacj e5382e5e86 Removed debug message
Removed debug message

Removed unused file
2026-02-11 15:40:12 +01:00
Snacj 5d8eca508a Adjusted Wago Coupler Logic to make Stepper Controller work. 2026-02-11 15:40:12 +01:00
Snacj 27e224fe5f Add debug 2026-02-11 15:40:12 +01:00
Snacj e61a92db1a Added Freq Prescaler and Acc Prescaler
Added Freq Prescaler and Acc Prescaler

Added Freq Prescaler and Acc Prescaler
2026-02-11 15:40:12 +01:00
just-some-entity 18428ceb75 WIP - debugging errors.
bullshit changes for bullshit device or idk
2026-02-11 15:40:12 +01:00
Snacj edb3cfbe7e Refactor 2026-02-11 15:40:12 +01:00
Snacj 1e4a5bbb31 Changed starting Velocity and Acceleration for testing 2026-02-11 15:40:12 +01:00
Snacj a90a1c44e7 Added Minimal Machine for Wago Stepper Controller 2026-02-11 15:40:12 +01:00
Snacj 87db200442 Motor Running - now move tick() function somewhere else. 2026-02-11 15:40:12 +01:00
Snacj 2c49bd4210 Fixed Compilation errors 2026-02-11 15:40:12 +01:00
Snacj a7c98339ab refactor rxpdo txpdo 2026-02-11 15:40:12 +01:00
Snacj 2e283affe2 Added stepper velocity helper 2026-02-11 15:40:12 +01:00
Snacj 5c4a6a30eb Refactor Control and Statusbytes into their own Structs. 2026-02-11 15:40:12 +01:00
Snacj 78302a0cdd Refactor of Offsets 2026-02-11 15:40:12 +01:00
Snacj 716c94060b Motor Running 2026-02-11 15:40:12 +01:00
Snacj 51bbf618fe Added infrastructure to control speed 2026-02-11 15:40:12 +01:00
Snacj b90286bd70 safing progress 2026-02-11 15:40:12 +01:00
Snacj 8ccfb9c33b Added driver to Minimal Machine 2026-02-11 15:40:12 +01:00
Snacj 3afcdf1d41 Added Driver Architecture for Stepper Controller and samll testing machine 2026-02-11 15:40:12 +01:00
snacj f370105fe8 Added Debug for testing rx_pdo 2026-02-11 15:40:12 +01:00
Snacj c4f3dcf6cd Added base of wago Stepper Controller 2026-02-11 15:40:12 +01:00
21 changed files with 1447 additions and 49 deletions

No files matched your search

+13 -36
View File
@@ -162,7 +162,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -659,7 +658,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -683,7 +681,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -3290,7 +3287,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz",
"integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0",
@@ -3381,7 +3377,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz",
"integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/store": "^0.8.0",
@@ -3602,7 +3597,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -3768,7 +3764,6 @@
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3785,7 +3780,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3796,7 +3790,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3884,7 +3877,6 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -4239,7 +4231,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4303,6 +4294,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -4617,7 +4609,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4973,8 +4964,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -5237,7 +5227,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -5783,7 +5774,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5844,7 +5834,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -5946,7 +5935,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -6905,7 +6893,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -6939,7 +6926,6 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -7542,7 +7528,6 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -8069,6 +8054,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -9489,7 +9475,6 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9606,6 +9591,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -9621,6 +9607,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9710,7 +9697,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9720,7 +9706,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9733,7 +9718,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9777,7 +9761,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -10291,7 +10276,6 @@
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz",
"integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -10833,8 +10817,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -10926,7 +10909,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11170,7 +11152,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11507,7 +11488,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -11597,8 +11577,7 @@
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
@@ -11636,7 +11615,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12076,7 +12054,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+25
View File
@@ -549,6 +549,30 @@ export const ip20TestMachine: MachineProperties = {
],
};
export const testmachinestepper: MachineProperties = {
name: "TestMachineStepper",
version: "V1",
slug: "testmachinestepper",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0037,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 0x00000021,
product_id: 0x07500354,
revision: 0x2,
},
],
},
],
};
export const TestMotor: MachineProperties = {
name: "TestMotor",
version: "V1",
@@ -623,6 +647,7 @@ export const machineProperties: MachineProperties[] = [
digitalInputTestMachine,
ip20TestMachine,
TestMotor,
testmachinestepper,
];
export const getMachineProperties = (
@@ -0,0 +1,130 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { useTestMachineStepper } from "./useTestMachineStepper";
import { Label } from "@/control/Label";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { TouchButton } from "@/components/touch/TouchButton";
export function TestMachineStepperControlPage() {
const { state, setTargetSpeed, setEnabled, setFreq, setAccFreq } =
useTestMachineStepper();
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Stepper Speed">
<Label label="Target Speed">
<EditValue
value={state?.target_speed}
unit="rpm"
title="Target Speed"
defaultValue={0}
min={-25000}
max={25000}
step={1}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setTargetSpeed}
/>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setEnabled(true)}
disabled={false}
isLoading={false}
>
Enable
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setEnabled(false)}
disabled={false}
isLoading={false}
>
Disable
</TouchButton>
</Label>
</ControlCard>
<ControlCard height={2} title="Frequency Prescaler">
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(0)}
disabled={false}
isLoading={false}
>
Default (00)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(1)}
disabled={false}
isLoading={false}
>
Low (01)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(2)}
disabled={false}
isLoading={false}
>
Mid (10)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(3)}
disabled={false}
isLoading={false}
>
High (11)
</TouchButton>
<ControlCard height={2} title="Acceleration Factor">
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(0)}
disabled={false}
isLoading={false}
>
Default (00)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(1)}
disabled={false}
isLoading={false}
>
Low (01)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(2)}
disabled={false}
isLoading={false}
>
Mid (10)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(3)}
disabled={false}
isLoading={false}
>
High (11)
</TouchButton>
</ControlCard>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,20 @@
import { Topbar } from "@/components/Topbar";
import { testMachineStepperSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMachineStepperPage() {
const { serial } = testMachineStepperSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmachinestepper/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -0,0 +1,81 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
target_speed: z.number(),
enabled: z.boolean(),
freq: z.number(),
acc_freq: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type TestMachineStepperNamespaceStore = {
state: StateEvent | null;
};
export const createTestMachineStepperNamespaceStore =
(): StoreApi<TestMachineStepperNamespaceStore> =>
create<TestMachineStepperNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMachineStepperMessageHandler(
store: StoreApi<TestMachineStepperNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMachineStepperNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: TestMachineStepperNamespaceStore,
) => TestMachineStepperNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useTestMachineStepperNamespaceImplementation =
createNamespaceHookImplementation<TestMachineStepperNamespaceStore>({
createStore: createTestMachineStepperNamespaceStore,
createEventHandler: testMachineStepperMessageHandler,
});
export function useTestMachineStepperNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMachineStepperNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMachineStepperNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,126 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMachineStepperSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import {
useTestMachineStepperNamespace,
StateEvent,
} from "./testMachineStepperNamespace";
import { useMachineMutate } from "@/client/useClient";
import { current, produce } from "immer";
import { useEffect, useMemo } from "react";
import { testmachinestepper } from "@/machines/properties";
import { z } from "zod";
export function useTestMachineStepper() {
const { serial: serialString } = testMachineStepperSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: testmachinestepper.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMachineStepperNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const setTargetSpeed = (target: number) => {
updateStateOptimistically(
(current) => {
current.target_speed = target;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetTargetSpeed", value: { target } },
}),
);
};
const setEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.enabled = enabled;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetEnabled", value: { enabled } },
}),
);
};
const setFreq = (factor: number) => {
updateStateOptimistically(
(current) => {
current.freq = factor;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetFreq", value: { factor } },
}),
);
};
const setAccFreq = (factor: number) => {
updateStateOptimistically(
(current) => {
current.acc_freq = factor;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetAccFreq", value: { factor } },
}),
);
};
return {
state: stateOptimistic.value,
setTargetSpeed,
setEnabled,
setFreq,
setAccFreq,
};
}
+19
View File
@@ -57,6 +57,8 @@ import { Aquapath1SettingsPage } from "@/machines/aquapath/aquapath1/Aquapath1Se
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { TestMachineStepperPage } from "@/machines/testmachinestepper/TestMachineStepperPage";
import { TestMachineStepperControlPage } from "@/machines/testmachinestepper/TestMachineStepperControlPage";
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
@@ -156,6 +158,19 @@ export const testMotorControlRoute = createRoute({
component: () => <TestMotorControlPage />,
});
export const testMachineStepperSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmachinestepper/$serial",
component: () => <TestMachineStepperPage />,
});
export const testMachineStepperControlRoute = createRoute({
getParentRoute: () => testMachineStepperSerialRoute,
path: "control",
component: () => <TestMachineStepperControlPage />,
});
// Leaf route: control page
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
path: "_sidebar",
@@ -487,6 +502,10 @@ export const rootTree = RootRoute.addChildren([
testMachineSerialRoute.addChildren([testMachineControlRoute]),
testMachineStepperSerialRoute.addChildren([
testMachineStepperControlRoute,
]),
analogInputTestMachineSerialRoute.addChildren([
analogInputTestMachineControlRoute,
]),
+30 -8
View File
@@ -15,6 +15,7 @@ use crate::{
wago_750_501::{WAGO_750_501_MODULE_IDENT, WAGO_750_501_PRODUCT_ID},
wago_750_530::{WAGO_750_530_MODULE_IDENT, WAGO_750_530_PRODUCT_ID},
wago_750_652::{WAGO_750_652_MODULE_IDENT, WAGO_750_652_PRODUCT_ID},
wago_750_671::{WAGO_750_671_MODULE_IDENT, WAGO_750_671_PRODUCT_ID},
wago_750_1506::{WAGO_750_1506_MODULE_IDENT, WAGO_750_1506_PRODUCT_ID},
},
},
@@ -62,7 +63,6 @@ impl EthercatDevice for Wago750_354 {
None => break,
}
}
Ok(())
}
@@ -153,14 +153,29 @@ impl std::fmt::Debug for Wago750_354 {
impl Wago750_354 {
pub fn calculate_module_index(pdo_mapping: u32, is_tx: bool) -> u32 {
let start_index = match is_tx {
true => 0x6000 as u32,
false => 0x7000 as u32,
true => 0x6000,
false => 0x7000,
};
let index_in_hex = ((pdo_mapping & 0xFFFF0000) >> 16) - start_index;
let pdo_index = (pdo_mapping & 0xFFFF0000) >> 16;
// Check if the PDO index is in the expected range
if pdo_index < start_index {
tracing::warn!(
"PDO mapping 0x{:08X} has index 0x{:04X} < expected start 0x{:04X}",
pdo_mapping,
pdo_index,
start_index
);
return 0; // Treat as coupler/module 0
}
let index_in_hex = pdo_index - start_index;
if index_in_hex < 16 {
return 0;
0
} else {
return index_in_hex / 16;
index_in_hex / 16
}
}
@@ -191,7 +206,7 @@ impl Wago750_354 {
}
let mut mappings_without_coupler: Vec<u32> = vec![];
for i in start_subindex..count_mappings {
for i in start_subindex..=count_mappings {
let pdo_index = device.sdo_read(index.0, i).await?;
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
for j in 0..pdo_map_count {
@@ -303,6 +318,11 @@ impl Wago750_354 {
module.has_rx = false;
module.name = "750-430".to_string();
}
WAGO_750_671_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
module.name = "750-671".to_string();
}
_ => println!(
"Wago-750-354 found Unknown/Unimplemented Module: {}",
ident_iom
@@ -383,7 +403,9 @@ impl Wago750_354 {
WAGO_750_430_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_430::Wago750_430::new()))
}
WAGO_750_671_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_671::Wago750_671::new()))
}
_ => {
println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
@@ -6,3 +6,4 @@ pub mod wago_750_455;
pub mod wago_750_501;
pub mod wago_750_530;
pub mod wago_750_652;
pub mod wago_750_671;
@@ -143,7 +143,7 @@ pub struct Wago750_1506 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
// Should always have on but not when calling constructor
// Should always have one but not when calling constructor
module: Option<Module>,
rx_pdo: Wago750_1506RxPdo,
tx_pdo: Wago750_1506TxPdo,
@@ -0,0 +1,355 @@
/*
* Wago Stepper Controller 750-671
* 24 VDC / 1.5 A
*/
use anyhow::Ok;
use bitvec::field::BitField;
use crate::{
devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
},
helpers::counter_wrapper_u16_i128::CounterWrapperU16U128,
io::stepper_velocity_wago_750_671::{
C1Flag, C1Mode, C2Flag, C3Flag, ControlByteC1, ControlByteC2, ControlByteC3, StatusByteS1,
StatusByteS2, StatusByteS3,
},
};
#[derive(Clone)]
pub struct Wago750_671 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub rxpdo: Wago750_671RxPdo,
pub txpdo: Wago750_671TxPdo,
module: Option<Module>,
pub state: InitState,
pub initialized: bool,
}
// unfortunately we need to track and set bits over multiple cycles
// and wait for their acknowledgement so we have to use a state machine
// inside of our loop.
#[derive(Debug, Clone)]
pub enum InitState {
Off,
Enable,
SetMode,
StartPulseStart,
StartPulseEnd,
Running,
ErrorQuit,
ResetQuit,
}
/*
* Wago has two different kind of applications when it comes to
* running the Stepper Controller.
* - Positioning
* - Frequency/Speed Control
* we always want to use the latter one so the following
* process images are for that usecase.
*/
#[derive(Clone, Debug, Default)]
pub struct Wago750_671RxPdo {
pub c0: u8, // C0
pub velocity: i16, // D0/D1
pub acceleration: u16, // D2/D3
pub c3: u8, // C3
pub c2: u8, // C2
pub c1: u8, // C1
}
#[derive(Clone, Debug, Default)]
pub struct Wago750_671TxPdo {
pub s0: u8, // S0
pub actual_velocity: i16, // D0/D1
pub position_l: u8, // D4
pub position_m: u8, // D5
pub position_h: u8, // D6
pub s3: u8, // S3
pub s2: u8, // S2
pub s1: u8, // S1
}
impl EthercatDeviceUsed for Wago750_671 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_671 {}
impl EthercatDynamicPDO for Wago750_671 {
fn get_tx_offset(&self) -> usize {
self.tx_bit_offset
}
fn get_rx_offset(&self) -> usize {
self.rx_bit_offset
}
fn set_tx_offset(&mut self, offset: usize) {
self.tx_bit_offset = offset
}
fn set_rx_offset(&mut self, offset: usize) {
self.rx_bit_offset = offset
}
}
impl EthercatDeviceProcessing for Wago750_671 {
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
}
impl EthercatDevice for Wago750_671 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.tx_bit_offset;
let mut b = [0u8; 12];
for i in 0..12 {
b[i] = input[base + i * 8..base + (i + 1) * 8].load_le();
}
self.txpdo = Wago750_671TxPdo {
s0: b[0],
actual_velocity: i16::from_le_bytes([b[2], b[3]]),
position_l: b[6],
position_m: b[7],
position_h: b[8],
s3: b[9],
s2: b[10],
s1: b[11],
};
// Yes this statemachine is unfortunately needed in here to make sure
// bits are correctly set and reset in the correct cycle.
match self.state {
InitState::Off => {
// Do nothing
self.initialized = false;
}
InitState::Enable => {
// set the specific bits of Control Byte C1
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.bits();
self.rxpdo.c1 = c1;
// Switch state if ENABLE and STOP2_N are acknowledged
// since S1 mirrors C1 with acknowledgements we can reuse c1 to compare
if self.txpdo.s1 == c1 {
self.state = InitState::SetMode;
}
}
InitState::SetMode => {
// set the specific bits of Control Byte C1 and also the mode
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_mode(C1Mode::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Switch state if SPEED MODE is acknowledged
if self.txpdo.s1 == c1 {
self.initialized = true;
self.state = InitState::StartPulseStart;
}
}
InitState::StartPulseStart => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_flag(C1Flag::Start)
.with_mode(C1Mode::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Switch state after StartPulse is acknowledged
if self.txpdo.s1 == c1 {
self.state = InitState::StartPulseEnd;
}
}
InitState::StartPulseEnd => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_mode(C1Mode::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Switch state after StartPulse is over
if self.txpdo.s1 == c1 {
self.state = InitState::Running;
}
}
InitState::Running => {
let c2 = ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
let c3 = ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
// Check for Error
if self.txpdo.s2 & c2 != 0 {
self.state = InitState::ErrorQuit;
} else {
self.rxpdo.c2 |= c2;
}
// Check for Reset
if self.txpdo.s3 & c3 != 0 {
self.state = InitState::ResetQuit;
} else {
self.rxpdo.c3 |= c3;
}
}
InitState::ErrorQuit => {
self.rxpdo.c2 |= ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
tracing::error!("Stepper Controller Errored. Trying to reenable...");
self.state = InitState::Enable;
}
InitState::ResetQuit => {
self.rxpdo.c3 |= ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
tracing::error!("Stepper Controller Reset not Quit. Trying to reenable...");
self.state = InitState::Enable;
}
}
Ok(())
}
fn input_len(&self) -> usize {
12 * 8
}
fn output(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.rx_bit_offset;
let b = [
self.rxpdo.c0,
0,
self.rxpdo.velocity.to_le_bytes()[0],
self.rxpdo.velocity.to_le_bytes()[1],
self.rxpdo.acceleration.to_le_bytes()[0],
self.rxpdo.acceleration.to_le_bytes()[1],
0,
0,
0,
self.rxpdo.c3,
self.rxpdo.c2,
self.rxpdo.c1,
];
for i in 0..12 {
output[base + i * 8..base + (i + 1) * 8].store_le(b[i]);
}
Ok(())
}
fn output_len(&self) -> usize {
12 * 8
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_module(&self) -> bool {
true
}
fn input_checked(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let expected = self.input_len();
let actual = input.len();
if actual != expected {
return Err(anyhow::anyhow!(
"[{}::Device::input_checked] Input length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
Ok(())
}
fn output_checked(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let expected = self.output_len();
let actual = output.len();
if actual != expected {
return Err(anyhow::anyhow!(
"[{}::Device::output_checked] Output length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
Ok(())
}
fn get_module(&self) -> Option<crate::devices::Module> {
self.module.clone()
}
fn set_module(&mut self, module: crate::devices::Module) {
self.tx_bit_offset = module.tx_offset;
self.rx_bit_offset = module.rx_offset;
self.module = Some(module)
}
}
impl NewEthercatDevice for Wago750_671 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
rxpdo: Wago750_671RxPdo::default(),
txpdo: Wago750_671TxPdo::default(),
state: InitState::Off,
initialized: false,
}
}
}
impl std::fmt::Debug for Wago750_671 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_671")
}
}
pub const WAGO_750_671_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_671_PRODUCT_ID: u32 = 108074216;
pub const WAGO_750_671_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_671_VENDOR_ID, WAGO_750_671_PRODUCT_ID);
@@ -5,7 +5,7 @@ const U16_MAX: i128 = std::u16::MAX as i128;
/// We convert the overflows and underflows to an i128 value for easier calculations.
///
/// When overriding the counter we don't set the valeu direclty but schedula it wiht the [`push_override`] so it can be synced with [`pop_override`] to an EtherCAT device.
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct CounterWrapperU16U128 {
counter: i128,
last_counter: u16,
+1
View File
@@ -7,4 +7,5 @@ pub mod encoder_input;
pub mod pulse_train_output;
pub mod serial_interface;
pub mod stepper_velocity_el70x1;
pub mod stepper_velocity_wago_750_671;
pub mod temperature_input;
@@ -0,0 +1,355 @@
use std::sync::Arc;
use smol::{block_on, lock::RwLock};
use crate::devices::wago_modules::wago_750_671::{InitState, Wago750_671};
/*
* Wago Stepper Velocity Wrapper around Stepper Controller
*
*/
#[derive(Debug)]
pub struct StepperVelocityWago750671 {
pub device: Arc<RwLock<Wago750_671>>,
pub state: InitState,
pub target_velocity: i16,
pub target_acceleration: u16,
pub enabled: bool,
pub freq_range_sel: u8,
pub acc_range_sel: u8,
}
impl StepperVelocityWago750671 {
pub fn new(device: Arc<RwLock<Wago750_671>>) -> Self {
Self {
device,
state: InitState::Off,
target_velocity: 1000,
target_acceleration: 10000,
enabled: false,
freq_range_sel: 0,
acc_range_sel: 0,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
if self.enabled && enabled {
return;
}
self.enabled = enabled;
if enabled {
self.change_init_state(InitState::Enable);
} else {
self.change_init_state(InitState::Off);
self.write_control_byte(ControlByte::C1, 0b00000000);
}
}
pub fn set_velocity(&mut self, velocity: i16) {
self.target_velocity = velocity;
let mut dev = block_on(self.device.write());
dev.rxpdo.velocity = velocity;
dev.rxpdo.acceleration = 10000; // hardcoded for now
if dev.initialized {
dev.state = InitState::StartPulseStart;
}
}
pub fn set_acceleration(&mut self, acceleration: u16) {
self.target_acceleration = acceleration;
let mut dev = block_on(self.device.write());
dev.rxpdo.acceleration = acceleration;
}
pub fn set_freq_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.freq_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
.with_freq_range(factor)
.bits();
dev.rxpdo.c2 = c2;
}
pub fn set_acc_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.acc_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
.with_acc_range(factor)
.bits();
dev.rxpdo.c2 = c2;
}
fn change_init_state(&mut self, state: InitState) {
self.state = state.clone();
let mut dev = block_on(self.device.write());
dev.state = state;
}
fn write_control_byte(&self, control_byte: ControlByte, value: u8) {
let mut dev = block_on(self.device.write());
match control_byte {
ControlByte::C0 => dev.rxpdo.c0 = value,
ControlByte::C1 => dev.rxpdo.c1 = value,
ControlByte::C2 => dev.rxpdo.c2 = value,
ControlByte::C3 => dev.rxpdo.c3 = value,
}
}
fn read_status_byte(&self, status_byte: StatusByte) -> u8 {
let dev = block_on(self.device.write());
match status_byte {
StatusByte::S0 => dev.txpdo.s0,
StatusByte::S1 => dev.txpdo.s1,
StatusByte::S2 => dev.txpdo.s2,
StatusByte::S3 => dev.txpdo.s3,
}
}
}
// The Different Control Bytes set control the stepper controller
// by setting and resetting specific bits.
//
// There are some construction functions provided to create the
// control bytes correctly.
/// Control Byte C1
#[derive(Clone, Copy, Default)]
pub struct ControlByteC1(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C1Flag {
Enable = 0b0000_0001,
Stop2N = 0b0000_0010,
Start = 0b0000_0100,
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C1Mode {
SpeedControl = 0b0000_1000,
Program = 0b0001_0000,
Reference = 0b0010_0000,
JogMode = 0b0100_0000,
Mailbox = 0b1000_0000,
}
impl ControlByteC1 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C1Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn with_mode(mut self, cmd: C1Mode) -> Self {
self.0 = (self.0 & 0b0000_0111) | (cmd as u8);
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Control Byte C2
#[derive(Clone, Copy, Default)]
pub struct ControlByteC2(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C2Flag {
FreqRangeSelL = 0b0000_0001,
FreqRangeSelH = 0b0000_0010,
AccRangeSelL = 0b0000_0100,
AccRangeSelH = 0b0000_1000,
PreCalc = 0b0100_0000,
ErrorQuit = 0b1000_0000,
}
impl ControlByteC2 {
pub const fn new() -> Self {
Self(0)
}
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn with_freq_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_1100) | (sel & 0b0000_0011);
self
}
pub const fn with_acc_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_0011) | ((sel & 0b0000_0011) << 2);
self
}
pub const fn with_flag(mut self, flag: C2Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Control Byte C3
#[derive(Clone, Copy, Default)]
pub struct ControlByteC3(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C3Flag {
SetActualPos = 0b0000_0001,
DirectionPos = 0b0000_0010,
DirectionNeg = 0b0000_0100,
ResetQuit = 0b1000_0000,
}
impl ControlByteC3 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C3Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
// The status bytes are similar to the control bytes but are only readable
// and most of the time provide corresponding acknoledgements to the diffrent
// Control bytes.
//
// There are also helper functions to construct the diffrent Status bytes for
// compoarison.
/// Status Byte S1
#[derive(Clone, Copy, Default)]
pub struct StatusByteS1(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S1Flag {
Ready = 0b0000_0001,
Stop2NAck = 0b0000_0010,
StartAck = 0b0000_0100,
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S1CommandAck {
Idle = 0b0000_0000,
SinglePosition = 0b0000_1000,
RunProgram = 0b0001_0000,
SpeedControl = 0b0001_1000,
Reference = 0b0010_0000,
JogMode = 0b0010_1000,
Mailbox = 0b0011_0000,
}
impl StatusByteS1 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S1Flag) -> bool {
(self.0 & flag as u8) != 0
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Status Byte S2
#[derive(Clone, Copy, Default)]
pub struct StatusByteS2(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S2Flag {
OnTarget = 0b0000_0001,
Busy = 0b0000_0010,
StandStill = 0b0000_0100,
OnSpeed = 0b0000_1000,
Direction = 0b0001_0000,
ReferenceOk = 0b0010_0000,
PreCalcAck = 0b0100_0000,
Error = 0b1000_0000,
}
impl StatusByteS2 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S2Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
/// Status Byte S3
#[derive(Clone, Copy, Default)]
pub struct StatusByteS3(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S3Flag {
Input1 = 0b0000_0001,
Input2 = 0b0000_0010,
Input3 = 0b0000_0100,
Input4 = 0b0000_1000,
Input5 = 0b0001_0000,
Input6 = 0b0010_0000,
Warning = 0b0100_0000,
Reset = 0b1000_0000,
}
impl StatusByteS3 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S3Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
#[derive(Debug, Clone, Copy)]
pub enum ControlByte {
C0,
C1,
C2,
C3,
}
#[derive(Debug, Clone, Copy)]
pub enum StatusByte {
S0,
S1,
S2,
S3,
}
+2 -1
View File
@@ -32,6 +32,7 @@ pub mod motor_test_machine;
pub mod registry;
pub mod serial;
pub mod test_machine;
pub mod test_machine_stepper;
pub mod wago_ai_test_machine;
pub mod wago_power;
pub mod winder2;
@@ -50,8 +51,8 @@ pub const IP20_TEST_MACHINE: u16 = 0x0034;
pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
pub const MOTOR_TEST_MACHINE: u16 = 0x0011;
pub const TEST_MACHINE_STEPPER: u16 = 0x0037;
use serde_json::Value;
use smol::lock::RwLock;
+2 -1
View File
@@ -20,7 +20,7 @@ use crate::{
winder2::Winder2,
};
use crate::test_machine::TestMachine;
use crate::{test_machine::TestMachine, test_machine_stepper::TestMachineStepper};
use lazy_static::lazy_static;
@@ -137,6 +137,7 @@ lazy_static! {
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
mc.register::<TestMachineStepper>(TestMachineStepper::MACHINE_IDENTIFICATION);
mc
};
}
+35
View File
@@ -0,0 +1,35 @@
use smol::block_on;
use super::TestMachineStepper;
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
impl MachineAct for TestMachineStepper {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {}
MachineMessage::DisconnectMachine(_machine_connection) => {}
MachineMessage::RequestValues(sender) => {}
}
}
}
+87
View File
@@ -0,0 +1,87 @@
use super::TestMachineStepper;
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub target_speed: i16,
pub enabled: bool,
pub freq: u8,
pub acc_freq: u8,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum TestMachineStepperEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetTargetSpeed { target: i16 },
SetEnabled { enabled: bool },
SetFreq { factor: u8 },
SetAccFreq { factor: u8 },
}
#[derive(Debug, Clone)]
pub struct TestMachineStepperNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<TestMachineStepperEvents> for TestMachineStepperNamespace {
fn emit(&mut self, events: TestMachineStepperEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<TestMachineStepperEvents> for TestMachineStepperEvents {
fn event_value(&self) -> GenericEvent {
match self {
TestMachineStepperEvents::State(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for TestMachineStepper {
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetTargetSpeed { target } => self.set_target_speed(target),
Mutation::SetEnabled { enabled } => self.set_enabled(enabled),
Mutation::SetFreq { factor } => self.set_freq(factor),
Mutation::SetAccFreq { factor } => self.set_acc_freq(factor),
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
+76
View File
@@ -0,0 +1,76 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::test_machine_stepper::api::{StateEvent, TestMachineStepperEvents};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::stepper_velocity_wago_750_671::StepperVelocityWago750671;
use smol::channel::{Receiver, Sender};
use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::test_machine_stepper::api::TestMachineStepperNamespace;
use crate::{TEST_MACHINE_STEPPER, VENDOR_QITECH};
#[derive(Debug)]
pub struct TestMachineStepper {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub namespace: TestMachineStepperNamespace,
pub last_state_emit: Instant,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub stepper: StepperVelocityWago750671,
}
impl Machine for TestMachineStepper {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl TestMachineStepper {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: TEST_MACHINE_STEPPER,
};
}
impl TestMachineStepper {
pub fn get_state(&self) -> StateEvent {
StateEvent {
target_speed: self.stepper.target_velocity,
enabled: self.stepper.enabled,
freq: self.stepper.freq_range_sel,
acc_freq: self.stepper.acc_range_sel,
}
}
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(TestMachineStepperEvents::State(event));
}
pub fn set_target_speed(&mut self, speed: i16) {
self.stepper.set_velocity(speed);
self.emit_state();
}
pub fn set_enabled(&mut self, enabled: bool) {
self.stepper.set_enabled(enabled);
self.emit_state();
}
pub fn set_freq(&mut self, factor: u8) {
self.stepper.set_freq_range_sel(factor);
self.emit_state();
}
pub fn set_acc_freq(&mut self, factor: u8) {
self.stepper.set_acc_range_sel(factor);
self.emit_state();
}
}
+86
View File
@@ -0,0 +1,86 @@
use crate::test_machine_stepper::{TestMachineStepper, api::TestMachineStepperNamespace};
use ethercat_hal::{
devices::{
EthercatDevice, downcast_device,
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
wago_modules::wago_750_671::Wago750_671,
},
io::stepper_velocity_wago_750_671::StepperVelocityWago750671,
};
use smol::{block_on, lock::RwLock};
use std::{sync::Arc, time::Instant};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
impl MachineNewTrait for TestMachineStepper {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
let device_identification = params
.device_group
.iter()
.map(|device_identification| device_identification.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/TestMachineStepper::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
let _wago_750_354 = get_ethercat_device::<Wago750_354>(
hardware,
params,
0,
[WAGO_750_354_IDENTITY_A].to_vec(),
)
.await?;
let modules = Wago750_354::initialize_modules(_wago_750_354.1).await?;
let mut coupler = _wago_750_354.0.write().await;
for module in modules {
coupler.set_module(module);
}
coupler.init_slot_modules(_wago_750_354.1);
let dev = coupler.slot_devices.first().unwrap().clone().unwrap();
// change uncomment this to change to different stepper driver
//
let wago_750_671: Arc<RwLock<Wago750_671>> =
downcast_device::<Wago750_671>(dev).await?;
// let wago_750_672: Arc<RwLock<Wago750_672>> =
// downcast_device::<Wago750_672>(dev).await?;
drop(coupler);
let stepper = StepperVelocityWago750671::new(wago_750_671);
let (sender, receiver) = smol::channel::unbounded();
let mut my_test = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: TestMachineStepperNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
main_sender: params.main_thread_channel.clone(),
stepper,
};
my_test.emit_state();
Ok(my_test)
})
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
npmDepsHash = "sha256-rnaAsSkoTRoBgLCTF8ZFpi7uUQ+8W+u3qoMxsptX6NU=";
npmDepsHash = "sha256-izbpayf659KJUAfdNwQ8rKBULNZLzsgTWSs1sGOj00A=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''