Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252fb31d65 | ||
|
|
bf12a25af4 | ||
|
|
c66f046a9b | ||
|
|
a7b1158f8d | ||
|
|
be87b377ba | ||
|
|
111664185e | ||
|
|
29ccaea7f2 | ||
|
|
8402199601 | ||
|
|
b7399c8d82 | ||
|
|
db71805912 | ||
|
|
3fd1b09589 | ||
|
|
c806f59df5 | ||
|
|
a5c0d75f33 | ||
|
|
9629652d4f | ||
|
|
5ee0b7e61b | ||
|
|
bb975081e8 | ||
|
|
6629055f9f | ||
|
|
3b10790146 | ||
|
|
cb6a954e3b | ||
|
|
bbf58e6b18 | ||
|
|
9af2364528 | ||
|
|
7d774b0025 | ||
|
|
94655093cc | ||
|
|
5b57273153 | ||
|
|
02951794f1 | ||
|
|
d1f96cc816 | ||
|
|
673b85420c | ||
|
|
493cb39396 | ||
|
|
34d6e85ec3 | ||
|
|
dd79586b13 | ||
|
|
fab801e62f | ||
|
|
37a1641450 | ||
|
|
6e4baaa356 | ||
|
|
3292c62212 | ||
|
|
3e1d83a0dd | ||
|
|
47dcd34364 | ||
|
|
2c134c4479 | ||
|
|
8613e0c5a8 | ||
|
|
02daf33e55 | ||
|
|
7b7eafee3b | ||
|
|
400fc016ae | ||
|
|
8b66ee415a | ||
|
|
89fef65347 | ||
|
|
5fbf5b7b97 | ||
|
|
8278bf579c | ||
|
|
6f7611ada5 | ||
|
|
a3ff479941 | ||
|
|
d14e1183d5 | ||
|
|
c7495a7d92 | ||
|
|
5428e950b0 |
No files matched your search
@@ -0,0 +1,124 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TimeAgnosticDeadTimePiController {
|
||||
// Params
|
||||
/// Proportional gain
|
||||
kp: f64,
|
||||
/// Integral gain
|
||||
ki: f64,
|
||||
// State
|
||||
/// Proportional error
|
||||
ep: f64,
|
||||
/// Integral error
|
||||
ei: f64,
|
||||
/// Dead Time
|
||||
dead: Duration,
|
||||
|
||||
last: Option<Instant>,
|
||||
}
|
||||
|
||||
impl TimeAgnosticDeadTimePiController {
|
||||
pub fn new(kp: f64, ki: f64, dead: Duration) -> Self {
|
||||
Self {
|
||||
kp,
|
||||
ki,
|
||||
dead,
|
||||
ep: 0.0,
|
||||
ei: 0.0,
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(&mut self, kp: f64, ki: f64) {
|
||||
self.reset();
|
||||
self.kp = kp;
|
||||
self.ki = ki;
|
||||
}
|
||||
|
||||
pub fn get_kp(&self) -> f64 {
|
||||
self.kp
|
||||
}
|
||||
|
||||
pub fn get_ki(&self) -> f64 {
|
||||
self.ki
|
||||
}
|
||||
|
||||
pub fn get_dead(&self) -> Duration {
|
||||
self.dead
|
||||
}
|
||||
|
||||
pub fn set_kp(&mut self, kp: f64) {
|
||||
self.kp = kp;
|
||||
}
|
||||
|
||||
pub fn set_ki(&mut self, ki: f64) {
|
||||
self.ki = ki;
|
||||
}
|
||||
|
||||
pub fn set_dead(&mut self, dead: Duration) {
|
||||
self.dead = dead;
|
||||
}
|
||||
|
||||
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
|
||||
match (min, max) {
|
||||
(Some(min), Some(max)) => value.clamp(min, max),
|
||||
(Some(min), None) => value.max(min),
|
||||
(None, Some(max)) => value.min(max),
|
||||
(None, None) => value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
|
||||
let kp = self.kp / 10000.0;
|
||||
let ki = self.ki / 10000.0;
|
||||
let signal = match self.last {
|
||||
// First update
|
||||
None => {
|
||||
let ep = error;
|
||||
|
||||
// Initialize integral error
|
||||
self.ei = 0.0;
|
||||
|
||||
// Proportional + Integral
|
||||
let signal = kp * ep + ki * self.ei;
|
||||
|
||||
// Save state
|
||||
self.ep = ep;
|
||||
self.last = Some(t);
|
||||
|
||||
-signal
|
||||
}
|
||||
// Subsequent updates
|
||||
Some(last) => {
|
||||
// Calculate elapsed time
|
||||
let dt = t.duration_since(last).as_secs_f64();
|
||||
|
||||
// Dead-time scaling factor
|
||||
let dead_secs = self.dead.as_secs_f64();
|
||||
let scale = (dt / dead_secs).min(1.0);
|
||||
|
||||
// Smoothed proportional error
|
||||
let ep = self.ep + (error - self.ep) * scale;
|
||||
|
||||
// Integrate error over time
|
||||
self.ei += error * dt;
|
||||
|
||||
// Proportional + Integral
|
||||
let signal = kp * ep + ki * self.ei;
|
||||
|
||||
// Save state
|
||||
self.ep = ep;
|
||||
self.last = Some(t);
|
||||
|
||||
-signal
|
||||
}
|
||||
};
|
||||
-signal
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.ep = 0.0;
|
||||
//self.last = None;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod clamping_timeagnostic_pid;
|
||||
pub mod deadtime_pi_controller;
|
||||
pub mod first_degree_motion;
|
||||
pub mod pid;
|
||||
pub mod second_degree_motion;
|
||||
@@ -92,7 +92,7 @@ pub struct MachineCrossConnection<
|
||||
> {
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
connected_machine: Weak<Mutex<MachineSlot<T>>>,
|
||||
pub connected_machine: Weak<Mutex<MachineSlot<T>>>,
|
||||
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
|
||||
// need F in our type arguemnts. We are ok with
|
||||
// it being used only in a fake field. Without
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import React from "react";
|
||||
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { TouchButton } from "./touch/TouchButton";
|
||||
|
||||
interface MachineIdentificationUnique {
|
||||
machine_identification: {
|
||||
@@ -49,67 +49,64 @@ export const MachineSelector: React.FC<MachineSelectorProps> = ({
|
||||
connectedMachineState,
|
||||
setConnectedMachine,
|
||||
clearConnectedMachine,
|
||||
title = "Machine Connection",
|
||||
}) => {
|
||||
return (
|
||||
<ControlCard title={title}>
|
||||
<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;
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<TouchButton className="text-md h-max px-6 py-6" variant="outline">
|
||||
<div className="flex flex-row items-center gap-2 text-wrap">
|
||||
<Icon name="lu:Link2" className="size-6" />
|
||||
{selectedMachine?.name ?? "Select a Machine"}{" "}
|
||||
{selectedMachine?.machine_identification_unique.serial ?? ""}
|
||||
</div>
|
||||
</TouchButton>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={machine.machine_identification_unique.serial}
|
||||
onClick={() =>
|
||||
setConnectedMachine(machine.machine_identification_unique)
|
||||
}
|
||||
className={`flex h-max cursor-pointer items-center gap-2 px-6 py-6${
|
||||
isSelected ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
{connectedMachineState?.is_available ? (
|
||||
<Icon name="lu:Link2" className="size-6 text-green-600" />
|
||||
) : (
|
||||
<Icon name="lu:Link2Off" className="size-6 text-black" />
|
||||
)}
|
||||
<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>
|
||||
</ControlCard>
|
||||
{clearConnectedMachine && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={clearConnectedMachine}
|
||||
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
|
||||
>
|
||||
<Icon name="lu:X" className="size-6 text-red-600" />
|
||||
<span>Disconnect Machine</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
variant: "error" | "success";
|
||||
variant: "error" | "success" | "info";
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -14,10 +14,18 @@ export function StatusBadge({ variant, children }: Props) {
|
||||
variant: {
|
||||
error: "bg-red-500",
|
||||
success: "bg-green-600",
|
||||
info: "bg-yellow-400 text-black",
|
||||
},
|
||||
},
|
||||
});
|
||||
const icon: IconName = variant === "error" ? "lu:TriangleAlert" : "lu:Check";
|
||||
|
||||
const icon: IconName =
|
||||
variant === "error"
|
||||
? "lu:TriangleAlert"
|
||||
: variant === "success"
|
||||
? "lu:Check"
|
||||
: "lu:Info";
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={badgeStyle({
|
||||
|
||||
@@ -72,6 +72,7 @@ export function Laser1ControlPage() {
|
||||
<div className="flex flex-row items-center gap-6">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Roundness"
|
||||
icon="lu:Circle"
|
||||
timeseries={roundness}
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,12 @@ export function Laser1Page() {
|
||||
title: "Graphs",
|
||||
icon: "lu:ChartSpline",
|
||||
},
|
||||
{
|
||||
link: "settings",
|
||||
activeLink: "settings",
|
||||
title: "Config",
|
||||
icon: "lu:Settings",
|
||||
},
|
||||
// {
|
||||
// link: "manual",
|
||||
// activeLink: "manual",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { Page } from "@/components/Page";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Label } from "@/control/Label";
|
||||
import { useLaser1 } from "./useLaser1";
|
||||
import { MachineSelector } from "@/components/MachineConnectionDropdown";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
|
||||
export function Laser1SettingsPage() {
|
||||
const {
|
||||
state,
|
||||
selectedMachine,
|
||||
filteredMachines,
|
||||
setConnectedWinder,
|
||||
disconnectWinder,
|
||||
} = useLaser1();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid>
|
||||
<ControlCard title="Speed by Diameter Settings">
|
||||
<Label label="Associated Winder">
|
||||
<MachineSelector
|
||||
machines={filteredMachines}
|
||||
selectedMachine={selectedMachine}
|
||||
connectedMachineState={state?.connected_winder_state}
|
||||
setConnectedMachine={setConnectedWinder}
|
||||
clearConnectedMachine={() => {
|
||||
if (!selectedMachine) return;
|
||||
setConnectedWinder({
|
||||
machine_identification: { vendor: 0, machine: 0 },
|
||||
serial: 0,
|
||||
});
|
||||
disconnectWinder(selectedMachine.machine_identification_unique);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,24 @@ import {
|
||||
} from "@/lib/timeseries";
|
||||
|
||||
// ========== Event Schema Definitions ==========
|
||||
/**
|
||||
* Connected machine state schema
|
||||
*/
|
||||
export const machineIdentificationSchema = z.object({
|
||||
vendor: z.number(),
|
||||
machine: z.number(),
|
||||
});
|
||||
|
||||
export const machineIdentificationUniqueSchema = z.object({
|
||||
machine_identification: machineIdentificationSchema,
|
||||
serial: z.number(),
|
||||
});
|
||||
|
||||
export const connectedMachineStateSchema = z.object({
|
||||
machine_identification_unique: machineIdentificationUniqueSchema.nullable(),
|
||||
is_available: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Live values from Laser (30 FPS)
|
||||
*/
|
||||
@@ -43,6 +61,7 @@ export const stateEventDataSchema = z.object({
|
||||
lower_tolerance: z.number(),
|
||||
target_diameter: z.number(),
|
||||
}),
|
||||
connected_winder_state: connectedMachineStateSchema,
|
||||
});
|
||||
|
||||
// ========== Event Schemas with Wrappers ==========
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { laser1 } from "@/machines/properties";
|
||||
import {
|
||||
machineIdentification,
|
||||
machineIdentificationUnique,
|
||||
MachineIdentificationUnique,
|
||||
} from "@/machines/types";
|
||||
import { laser1, VENDOR_QITECH } from "@/machines/properties";
|
||||
import { laser1SerialRoute } from "@/routes/routes";
|
||||
import { z } from "zod";
|
||||
import { useLaser1Namespace, StateEvent } from "./laser1Namespace";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { produce } from "immer";
|
||||
import { useMachines } from "@/client/useMachines";
|
||||
|
||||
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
|
||||
// Get consolidated state and live values from namespace
|
||||
@@ -54,6 +59,16 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
|
||||
const { request: requestHigherTolerance } = useMachineMutation(
|
||||
schemaHigherTolerance,
|
||||
);
|
||||
const { request: requestConnectedWinder } = useMachineMutation(
|
||||
z.object({
|
||||
SetConnectedWinder: machineIdentificationUnique,
|
||||
}),
|
||||
);
|
||||
const { request: requestDisconnectWinder } = useMachineMutation(
|
||||
z.object({
|
||||
DisconnectWinder: machineIdentificationUnique,
|
||||
}),
|
||||
);
|
||||
|
||||
// Action functions with verb-first names
|
||||
const setTargetDiameter = (target_diameter: number) => {
|
||||
@@ -101,10 +116,80 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
|
||||
);
|
||||
};
|
||||
|
||||
const setConnectedWinder = (machineIdentificationUnique: {
|
||||
machine_identification: {
|
||||
vendor: number;
|
||||
machine: number;
|
||||
};
|
||||
serial: number;
|
||||
}) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.connected_winder_state.machine_identification_unique =
|
||||
machineIdentificationUnique;
|
||||
},
|
||||
() =>
|
||||
requestConnectedWinder({
|
||||
machine_identification_unique,
|
||||
data: { SetConnectedWinder: machineIdentificationUnique },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const disconnectWinder = (machineIdentificationUnique: {
|
||||
machine_identification: {
|
||||
vendor: number;
|
||||
machine: number;
|
||||
};
|
||||
serial: number;
|
||||
}) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.connected_winder_state.machine_identification_unique =
|
||||
null;
|
||||
},
|
||||
() =>
|
||||
requestDisconnectWinder({
|
||||
machine_identification_unique,
|
||||
data: { DisconnectWinder: machineIdentificationUnique },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// General Helper functions
|
||||
const machines = useMachines();
|
||||
// Filter machines for the correct type
|
||||
const filteredMachines = useMemo(
|
||||
() =>
|
||||
machines.filter(
|
||||
(m) =>
|
||||
m.machine_identification_unique.machine_identification.vendor ===
|
||||
VENDOR_QITECH &&
|
||||
m.machine_identification_unique.machine_identification.machine ===
|
||||
0x0002,
|
||||
),
|
||||
[machines, machineIdentification],
|
||||
);
|
||||
|
||||
// Get selected machine by serial
|
||||
const selectedMachine = useMemo(() => {
|
||||
const serial =
|
||||
state?.data.connected_winder_state?.machine_identification_unique?.serial;
|
||||
|
||||
return (
|
||||
filteredMachines.find(
|
||||
(m) => m.machine_identification_unique.serial === serial,
|
||||
) ?? null
|
||||
);
|
||||
}, [filteredMachines, state]);
|
||||
|
||||
return {
|
||||
// Consolidated state
|
||||
state: stateOptimistic.value?.data,
|
||||
|
||||
filteredMachines,
|
||||
selectedMachine,
|
||||
|
||||
// Default state for initial values
|
||||
defaultState: defaultState?.data,
|
||||
|
||||
@@ -122,6 +207,8 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
|
||||
setTargetDiameter,
|
||||
setLowerTolerance,
|
||||
setHigherTolerance,
|
||||
setConnectedWinder,
|
||||
disconnectWinder,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -272,13 +272,18 @@ export function Winder2ControlPage() {
|
||||
Diameter: {
|
||||
children: "Diameter",
|
||||
icon: "lu:Sun",
|
||||
disabled: true,
|
||||
disabled: !state?.connected_laser_state.is_available,
|
||||
},
|
||||
}}
|
||||
onChange={setPullerRegulationMode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
/>
|
||||
{state?.connected_laser_state?.is_available !== true ? (
|
||||
<StatusBadge variant={"info"}>
|
||||
{"No associated Laser found"}
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
</Label>
|
||||
<Label label="Target Speed">
|
||||
<EditValue
|
||||
|
||||
@@ -40,10 +40,13 @@ export function Winder2SettingPage() {
|
||||
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
setSpeedPiKp,
|
||||
setSpeedPiKi,
|
||||
selectedMachine,
|
||||
filteredMachines,
|
||||
setConnectedLaser,
|
||||
disconnectLaser,
|
||||
setConnectedMachine,
|
||||
disconnectMachine,
|
||||
} = useWinder2();
|
||||
|
||||
const handleXlModeChange = (enabled: boolean) => {
|
||||
@@ -354,20 +357,48 @@ export function Winder2SettingPage() {
|
||||
/>
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
<ControlCard title="Diameter-Based Winder Control">
|
||||
<Label label="Associated Laser">
|
||||
<MachineSelector
|
||||
machines={filteredMachines}
|
||||
selectedMachine={selectedMachine}
|
||||
connectedMachineState={state?.connected_laser_state}
|
||||
setConnectedMachine={setConnectedLaser}
|
||||
clearConnectedMachine={() => {
|
||||
if (!selectedMachine) return;
|
||||
setConnectedMachine({
|
||||
machine_identification: { vendor: 0, machine: 0 },
|
||||
serial: 0,
|
||||
});
|
||||
disconnectLaser(selectedMachine.machine_identification_unique);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="PI Controller Kp Factor">
|
||||
<EditValue
|
||||
value={state?.pi_settings?.kp}
|
||||
defaultValue={defaultState?.pi_settings?.kp}
|
||||
min={0}
|
||||
max={1000}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={setSpeedPiKp}
|
||||
title="Speed P KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="PI Controller Ki Factor">
|
||||
<EditValue
|
||||
value={state?.pi_settings?.ki}
|
||||
defaultValue={defaultState?.pi_settings?.ki}
|
||||
min={0}
|
||||
max={1000}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={setSpeedPiKi}
|
||||
title="Speed P KI"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -179,6 +179,27 @@ export function useWinder2() {
|
||||
}),
|
||||
);
|
||||
|
||||
const { request: requestConnectedLaser } = useMachineMutation(
|
||||
z.object({
|
||||
SetConnectedLaser: machineIdentificationUnique,
|
||||
}),
|
||||
);
|
||||
|
||||
const { request: requestDisconnectLaser } = useMachineMutation(
|
||||
z.object({
|
||||
DisconnectLaser: machineIdentificationUnique,
|
||||
}),
|
||||
);
|
||||
|
||||
const { request: requestSpeedPiSettings } = useMachineMutation(
|
||||
z.object({
|
||||
SetSpeedPiSettings: z.object({
|
||||
kp: z.number(),
|
||||
ki: z.number(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Helper function for optimistic updates using produce
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
@@ -593,6 +614,85 @@ export function useWinder2() {
|
||||
);
|
||||
};
|
||||
|
||||
const setSpeedPiKp = (kp: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.pi_settings.kp = kp;
|
||||
},
|
||||
() => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState) {
|
||||
const settings = produce(currentState.data.pi_settings, (draft) => {
|
||||
draft.kp = kp;
|
||||
});
|
||||
requestSpeedPiSettings({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetSpeedPiSettings: settings },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setSpeedPiKi = (ki: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.pi_settings.ki = ki;
|
||||
},
|
||||
() => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState) {
|
||||
const settings = produce(currentState.data.pi_settings, (draft) => {
|
||||
draft.ki = ki;
|
||||
});
|
||||
requestSpeedPiSettings({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetSpeedPiSettings: settings },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setConnectedLaser = (machineIdentificationUnique: {
|
||||
machine_identification: {
|
||||
vendor: number;
|
||||
machine: number;
|
||||
};
|
||||
serial: number;
|
||||
}) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.connected_laser_state.machine_identification_unique =
|
||||
machineIdentificationUnique;
|
||||
},
|
||||
() =>
|
||||
requestConnectedLaser({
|
||||
machine_identification_unique,
|
||||
data: { SetConnectedLaser: machineIdentificationUnique },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const disconnectLaser = (machineIdentificationUnique: {
|
||||
machine_identification: {
|
||||
vendor: number;
|
||||
machine: number;
|
||||
};
|
||||
serial: number;
|
||||
}) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.connected_laser_state.machine_identification_unique = null;
|
||||
},
|
||||
() =>
|
||||
requestDisconnectLaser({
|
||||
machine_identification_unique,
|
||||
data: { DisconnectLaser: machineIdentificationUnique },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Calculate loading states
|
||||
const isLoading = stateOptimistic.isOptimistic;
|
||||
const isDisabled = !stateOptimistic.isInitialized;
|
||||
@@ -607,7 +707,7 @@ export function useWinder2() {
|
||||
m.machine_identification_unique.machine_identification.vendor ===
|
||||
VENDOR_QITECH &&
|
||||
m.machine_identification_unique.machine_identification.machine ===
|
||||
0x0008,
|
||||
0x0006,
|
||||
),
|
||||
[machines, machineIdentification],
|
||||
);
|
||||
@@ -615,8 +715,7 @@ export function useWinder2() {
|
||||
// Get selected machine by serial
|
||||
const selectedMachine = useMemo(() => {
|
||||
const serial =
|
||||
state?.data.connected_machine_state?.machine_identification_unique
|
||||
?.serial;
|
||||
state?.data.connected_laser_state?.machine_identification_unique?.serial;
|
||||
|
||||
return (
|
||||
filteredMachines.find(
|
||||
@@ -674,7 +773,11 @@ export function useWinder2() {
|
||||
setSpoolAdaptiveMaxSpeedMultiplier,
|
||||
setSpoolAdaptiveAccelerationFactor,
|
||||
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
|
||||
setSpeedPiKp,
|
||||
setSpeedPiKi,
|
||||
setConnectedMachine,
|
||||
disconnectMachine,
|
||||
setConnectedLaser,
|
||||
disconnectLaser,
|
||||
};
|
||||
}
|
||||
@@ -39,7 +39,10 @@ export const liveValuesEventDataSchema = z.object({
|
||||
/**
|
||||
* Puller regulation type enum
|
||||
*/
|
||||
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
|
||||
export const pullerRegulationSchema = z.enum([
|
||||
"Speed",
|
||||
"Diameter",
|
||||
]);
|
||||
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
|
||||
|
||||
/**
|
||||
@@ -141,7 +144,7 @@ export const modeStateSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Connected machine state scheme
|
||||
* Connected machine state schema
|
||||
*/
|
||||
export const machineIdentificationSchema = z.object({
|
||||
vendor: z.number(),
|
||||
@@ -158,6 +161,14 @@ export const connectedMachineStateSchema = z.object({
|
||||
is_available: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* PI Controller settings schema
|
||||
*/
|
||||
export const piSettingsSchema = z.object({
|
||||
kp: z.number(),
|
||||
ki: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Tension arm state schema
|
||||
*/
|
||||
@@ -192,6 +203,8 @@ export const stateEventDataSchema = z.object({
|
||||
spool_speed_controller_state: spoolSpeedControllerStateSchema,
|
||||
spool_automatic_action_state: spoolAutomaticActionStateSchema,
|
||||
connected_machine_state: connectedMachineStateSchema,
|
||||
connected_laser_state: connectedMachineStateSchema,
|
||||
pi_settings: piSettingsSchema,
|
||||
});
|
||||
|
||||
// ========== Event Schemas with Wrappers ==========
|
||||
|
||||
@@ -41,6 +41,7 @@ import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph
|
||||
|
||||
import { z } from "zod";
|
||||
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
|
||||
import { Laser1SettingsPage } from "@/machines/laser/laser1/Laser1Settings";
|
||||
// make a route tree like this
|
||||
// _mainNavigation/machines/winder2/$serial/control
|
||||
// _mainNavigation/configuration/a
|
||||
@@ -156,6 +157,12 @@ export const laser1PresetsRoute = createRoute({
|
||||
component: () => <Laser1PresetsPage />,
|
||||
});
|
||||
|
||||
export const laser1SettingsRoute = createRoute({
|
||||
getParentRoute: () => laser1SerialRoute,
|
||||
path: "settings",
|
||||
component: () => <Laser1SettingsPage />,
|
||||
});
|
||||
|
||||
export const mock1SerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "mock1/$serial",
|
||||
@@ -312,6 +319,7 @@ export const rootTree = RootRoute.addChildren([
|
||||
laser1ControlRoute,
|
||||
laser1GraphsRoute,
|
||||
laser1PresetsRoute,
|
||||
laser1SettingsRoute,
|
||||
]),
|
||||
|
||||
aquapath1SerialRoute.addChildren([
|
||||
|
||||
@@ -17,7 +17,7 @@ use control_core::machines::new::MachineNewHardwareEthercat;
|
||||
use control_core::realtime::{set_core_affinity, set_realtime_priority};
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use ethercat_hal::devices::devices_from_subdevices;
|
||||
use ethercrab::std::ethercat_now;
|
||||
use ethercrab::std::{ethercat_now, tx_rx_task};
|
||||
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
@@ -54,26 +54,12 @@ pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(),
|
||||
// Set the thread to real-time priority
|
||||
#[cfg(not(feature = "development-build"))]
|
||||
let _ = set_realtime_priority();
|
||||
|
||||
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
|
||||
{
|
||||
use ethercrab::std::tx_rx_task;
|
||||
use futures::executor::block_on;
|
||||
|
||||
let rt = smol::LocalExecutor::new();
|
||||
let _ = block_on(rt.run(async {
|
||||
tx_rx_task(&interface, tx, rx)
|
||||
.expect("spawn TX/RX task")
|
||||
.await
|
||||
}));
|
||||
}
|
||||
#[cfg(all(target_os = "linux", feature = "io-uring"))]
|
||||
{
|
||||
use ethercrab::std::tx_rx_task_io_uring;
|
||||
|
||||
let _ = tx_rx_task_io_uring(&interface, tx, rx)
|
||||
.expect("Failed to spawn TX/RX task (io_uring)");
|
||||
}
|
||||
let rt = smol::LocalExecutor::new();
|
||||
let _ = smol::block_on(rt.run(async {
|
||||
tx_rx_task(&interface, tx, rx)
|
||||
.expect("spawn TX/RX task")
|
||||
.await
|
||||
}));
|
||||
})
|
||||
.expect("Building thread");
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@ use std::time::{Duration, Instant};
|
||||
///
|
||||
impl MachineAct for LaserMachine {
|
||||
fn act(&mut self, now: Instant) {
|
||||
// sync the diameter
|
||||
self.set_measured_diameter();
|
||||
self.update();
|
||||
// more than 33ms have passed since last emit (30 "fps" target)
|
||||
// The live values are updated approximately 60 times per second
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::LaserMachine;
|
||||
use control_core::{
|
||||
machines::api::MachineApi,
|
||||
machines::{
|
||||
api::MachineApi, connection::MachineCrossConnectionState,
|
||||
identification::MachineIdentificationUnique,
|
||||
},
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
@@ -36,6 +39,8 @@ pub struct StateEvent {
|
||||
pub is_default_state: bool,
|
||||
/// laser state
|
||||
pub laser_state: LaserState,
|
||||
/// connected winder state
|
||||
pub connected_winder_state: MachineCrossConnectionState,
|
||||
}
|
||||
|
||||
impl StateEvent {
|
||||
@@ -54,6 +59,13 @@ pub struct LaserState {
|
||||
pub target_diameter: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ConnectedMachineState {
|
||||
/// Connected Machine
|
||||
pub machine_identification_unique: Option<MachineIdentificationUnique>,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
pub enum LaserEvents {
|
||||
LiveValues(Event<LiveValuesEvent>),
|
||||
State(Event<StateEvent>),
|
||||
@@ -91,6 +103,11 @@ enum Mutation {
|
||||
SetTargetDiameter(f64),
|
||||
SetLowerTolerance(f64),
|
||||
SetHigherTolerance(f64),
|
||||
|
||||
// Connect Machine
|
||||
SetConnectedWinder(MachineIdentificationUnique),
|
||||
// Disconnect Machine
|
||||
DisconnectWinder(MachineIdentificationUnique),
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<LaserEvents> for LaserMachineNamespace {
|
||||
@@ -117,6 +134,12 @@ impl MachineApi for LaserMachine {
|
||||
Mutation::SetTargetDiameter(target_diameter) => {
|
||||
self.set_target_diameter(target_diameter);
|
||||
}
|
||||
Mutation::SetConnectedWinder(machine_identification_unique) => {
|
||||
self.set_connected_winder(machine_identification_unique);
|
||||
}
|
||||
Mutation::DisconnectWinder(machine_identification_unique) => {
|
||||
self.disconnect_winder(machine_identification_unique);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
use crate::{
|
||||
machines::{MACHINE_LASER_V1, VENDOR_QITECH},
|
||||
machines::{MACHINE_LASER_V1, VENDOR_QITECH, winder2::Winder2},
|
||||
serial::devices::laser::Laser,
|
||||
};
|
||||
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
|
||||
use control_core::{
|
||||
machines::identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
machines::{
|
||||
connection::{CrossConnectableMachine, MachineConnection, MachineCrossConnection},
|
||||
identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
manager::MachineManager,
|
||||
},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use control_core_derive::Machine;
|
||||
use smol::lock::RwLock;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use std::{
|
||||
sync::{Arc, Weak},
|
||||
time::Instant,
|
||||
};
|
||||
use uom::si::{f64::Length, length::millimeter};
|
||||
|
||||
pub mod act;
|
||||
@@ -18,8 +25,6 @@ pub mod new;
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct LaserMachine {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// drivers
|
||||
laser: Arc<RwLock<Laser>>,
|
||||
|
||||
@@ -27,6 +32,13 @@ pub struct LaserMachine {
|
||||
namespace: LaserMachineNamespace,
|
||||
last_measurement_emit: Instant,
|
||||
|
||||
// machine connection
|
||||
pub machine_manager: Weak<RwLock<MachineManager>>,
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// connected machines
|
||||
pub connected_winder: MachineCrossConnection<LaserMachine, Winder2>,
|
||||
|
||||
// laser values
|
||||
diameter: Length,
|
||||
x_diameter: Option<Length>,
|
||||
@@ -41,6 +53,12 @@ pub struct LaserMachine {
|
||||
emitted_default_state: bool,
|
||||
}
|
||||
|
||||
impl CrossConnectableMachine<LaserMachine, Winder2> for LaserMachine {
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<LaserMachine, Winder2> {
|
||||
&mut self.connected_winder
|
||||
}
|
||||
}
|
||||
|
||||
impl LaserMachine {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
@@ -74,6 +92,7 @@ impl LaserMachine {
|
||||
StateEvent {
|
||||
is_default_state: false,
|
||||
laser_state: laser,
|
||||
connected_winder_state: self.connected_winder.to_state(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +104,7 @@ impl LaserMachine {
|
||||
lower_tolerance: self.laser_target.lower_tolerance.get::<millimeter>(),
|
||||
target_diameter: self.laser_target.diameter.get::<millimeter>(),
|
||||
},
|
||||
connected_winder_state: self.connected_winder.to_state(),
|
||||
};
|
||||
|
||||
self.namespace.emit(LaserEvents::State(state.build()));
|
||||
@@ -150,6 +170,70 @@ impl LaserMachine {
|
||||
}
|
||||
}
|
||||
|
||||
/// implement machine connection
|
||||
impl LaserMachine {
|
||||
/// set connected winder
|
||||
pub fn set_connected_winder(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
Winder2::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.connected_winder
|
||||
.set_connected_machine(&machine_identification_unique);
|
||||
self.connected_winder.reverse_connect();
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
/// disconnect winder
|
||||
pub fn disconnect_winder(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
Winder2::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.connected_winder.reverse_disconnect();
|
||||
self.connected_winder.disconnect();
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
impl LaserMachine {
|
||||
fn set_measured_diameter(&mut self) {
|
||||
let diameter = smol::block_on(async {
|
||||
self.laser
|
||||
.read()
|
||||
.await
|
||||
.get_data()
|
||||
.await
|
||||
.map(|laser_data| laser_data.diameter.get::<millimeter>())
|
||||
});
|
||||
|
||||
if let Some(slot) = self.get_cross_connection().connected_machine.upgrade() {
|
||||
let slot = slot.lock_blocking();
|
||||
|
||||
if let MachineConnection::Connected(machine) = &slot.machine_connection {
|
||||
let mut winder = machine.lock_blocking();
|
||||
|
||||
winder
|
||||
.puller_speed_controller
|
||||
.set_measured_diameter(diameter.unwrap_or(0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaserTarget {
|
||||
diameter: Length,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
|
||||
|
||||
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
|
||||
use anyhow::Error;
|
||||
use control_core::machines::connection::MachineCrossConnection;
|
||||
use control_core::machines::new::{MachineNewHardware, MachineNewTrait};
|
||||
use uom::ConstZero;
|
||||
use uom::si::{f64::Length, length::millimeter};
|
||||
@@ -34,6 +35,7 @@ impl MachineNewTrait for LaserMachine {
|
||||
lower_tolerance: Length::new::<millimeter>(0.05),
|
||||
diameter: Length::new::<millimeter>(1.75),
|
||||
};
|
||||
|
||||
let mut laser_machine = Self {
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
laser,
|
||||
@@ -43,6 +45,11 @@ impl MachineNewTrait for LaserMachine {
|
||||
last_measurement_emit: Instant::now(),
|
||||
laser_target,
|
||||
emitted_default_state: false,
|
||||
machine_manager: params.machine_manager.clone(),
|
||||
connected_winder: MachineCrossConnection::new(
|
||||
params.machine_manager.clone(),
|
||||
¶ms.get_machine_identification_unique(),
|
||||
),
|
||||
diameter: Length::ZERO,
|
||||
x_diameter: None,
|
||||
y_diameter: None,
|
||||
|
||||
@@ -135,11 +135,16 @@ pub enum Mutation {
|
||||
// Mode
|
||||
SetMode(Mode),
|
||||
|
||||
// Pid Configure
|
||||
SetSpeedPiSettings(PiSettings),
|
||||
|
||||
// Connected Machine
|
||||
SetConnectedMachine(MachineIdentificationUnique),
|
||||
SetConnectedLaser(MachineIdentificationUnique),
|
||||
|
||||
// Disconnect Machine
|
||||
DisconnectMachine(MachineIdentificationUnique),
|
||||
DisconnectLaser(MachineIdentificationUnique),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
@@ -177,8 +182,12 @@ pub struct StateEvent {
|
||||
pub tension_arm_state: TensionArmState,
|
||||
/// spool speed controller state
|
||||
pub spool_speed_controller_state: SpoolSpeedControllerState,
|
||||
/// connected machine state
|
||||
/// connected machine state (Buffer)
|
||||
pub connected_machine_state: MachineCrossConnectionState,
|
||||
/// connected laser state
|
||||
pub connected_laser_state: MachineCrossConnectionState,
|
||||
/// pi settings
|
||||
pub pi_settings: PiSettingsStates,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
@@ -251,7 +260,7 @@ pub struct ModeState {
|
||||
pub can_wind: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct TensionArmState {
|
||||
/// is zeroed
|
||||
pub zeroed: bool,
|
||||
@@ -279,6 +288,25 @@ pub struct SpoolSpeedControllerState {
|
||||
pub forward: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ConnectedMachineState {
|
||||
/// Connected Machine
|
||||
pub machine_identification_unique: Option<MachineIdentificationUnique>,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct PiSettings {
|
||||
pub kp: f64,
|
||||
pub ki: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct PiSettingsStates {
|
||||
pub kp: f64,
|
||||
pub ki: f64,
|
||||
}
|
||||
|
||||
pub enum Winder2Events {
|
||||
LiveValues(Event<LiveValuesEvent>),
|
||||
State(Event<StateEvent>),
|
||||
@@ -369,6 +397,15 @@ impl MachineApi for Winder2 {
|
||||
Mutation::DisconnectMachine(machine_identification_unique) => {
|
||||
self.disconnect_buffer(machine_identification_unique)
|
||||
}
|
||||
Mutation::SetConnectedLaser(machine_identification_unique) => {
|
||||
self.set_connected_laser(machine_identification_unique)
|
||||
}
|
||||
Mutation::DisconnectLaser(machine_identification_unique) => {
|
||||
self.disconnect_laser(machine_identification_unique)
|
||||
}
|
||||
Mutation::SetSpeedPiSettings(settings) => {
|
||||
self.configure_pi_controller(settings);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ mod winder2_imports {
|
||||
|
||||
pub use super::super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
|
||||
pub use crate::machines::buffer1::BufferV1;
|
||||
pub use crate::machines::laser::LaserMachine;
|
||||
pub use crate::machines::winder2::api::{PiSettings, PiSettingsStates};
|
||||
|
||||
pub use api::{
|
||||
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
|
||||
@@ -293,6 +295,11 @@ impl Winder2 {
|
||||
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
|
||||
},
|
||||
connected_machine_state: self.connected_buffer.to_state(),
|
||||
connected_laser_state: self.connected_laser.to_state(),
|
||||
pi_settings: PiSettingsStates {
|
||||
kp: self.puller_speed_controller.p_dead_controller.get_kp(),
|
||||
ki: self.puller_speed_controller.p_dead_controller.get_ki(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +537,39 @@ impl Winder2 {
|
||||
self.connected_buffer.reverse_disconnect();
|
||||
self.connected_buffer.disconnect();
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
/// set connected laser
|
||||
pub fn set_connected_laser(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
LaserMachine::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.connected_laser
|
||||
.set_connected_machine(&machine_identification_unique);
|
||||
self.connected_laser.reverse_connect();
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// disconnect laser
|
||||
pub fn disconnect_laser(&mut self, machine_identification_unique: MachineIdentificationUnique) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
LaserMachine::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.connected_laser.reverse_disconnect();
|
||||
self.connected_laser.disconnect();
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -46,9 +46,9 @@ mod winder2_imports {
|
||||
pub use std::{fmt::Debug, sync::Weak, time::Instant};
|
||||
pub use uom::si::f64::Length;
|
||||
|
||||
pub use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH, buffer1::BufferV1};
|
||||
pub use uom::ConstZero;
|
||||
pub use uom::si::{length::meter, length::millimeter, velocity::meter_per_second};
|
||||
pub use crate::machines::{buffer1::BufferV1, laser::LaserMachine, winder2::api::PiSettings, MACHINE_WINDER_V1, VENDOR_QITECH};
|
||||
}
|
||||
|
||||
pub use winder2_imports::*;
|
||||
@@ -96,6 +96,7 @@ pub struct Winder2 {
|
||||
|
||||
// connected machines
|
||||
pub connected_buffer: MachineCrossConnection<Winder2, BufferV1>,
|
||||
pub connected_laser: MachineCrossConnection<Winder2, LaserMachine>,
|
||||
|
||||
// mode
|
||||
pub mode: Winder2Mode,
|
||||
@@ -126,6 +127,12 @@ impl CrossConnectableMachine<Winder2, BufferV1> for Winder2 {
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl CrossConnectableMachine<Winder2, LaserMachine> for Winder2 {
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<Winder2, LaserMachine> {
|
||||
&mut self.connected_laser
|
||||
}
|
||||
}
|
||||
|
||||
impl Winder2 {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
@@ -331,6 +338,17 @@ impl Winder2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Winder2 {
|
||||
pub fn configure_pi_controller(&mut self, settings: PiSettings) {
|
||||
// Implement pid to controll speed of winder
|
||||
self.puller_speed_controller
|
||||
.p_dead_controller
|
||||
.configure(settings.kp, settings.ki);
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Winder2Mode {
|
||||
Standby,
|
||||
|
||||
@@ -190,14 +190,6 @@ impl MachineNewTrait for Winder2 {
|
||||
|
||||
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 mut new = Self {
|
||||
traverse: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
|
||||
traverse_end_stop: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
|
||||
@@ -242,11 +234,15 @@ impl MachineNewTrait for Winder2 {
|
||||
mode: super::api::SpoolAutomaticActionMode::NoAction,
|
||||
},
|
||||
machine_manager: params.machine_manager.clone(),
|
||||
machine_identification_unique: machine_id,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
connected_buffer: MachineCrossConnection::new(
|
||||
params.machine_manager.clone(),
|
||||
¶ms.get_machine_identification_unique(),
|
||||
),
|
||||
connected_laser: MachineCrossConnection::new(
|
||||
params.machine_manager.clone(),
|
||||
¶ms.get_machine_identification_unique(),
|
||||
),
|
||||
};
|
||||
|
||||
// initalize events
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use control_core::{
|
||||
controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
|
||||
controllers::{
|
||||
deadtime_pi_controller::TimeAgnosticDeadTimePiController,
|
||||
second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
|
||||
},
|
||||
converters::linear_step_converter::LinearStepConverter,
|
||||
uom_extensions::{
|
||||
acceleration::meter_per_minute_per_second, jerk::meter_per_minute_per_second_squared,
|
||||
@@ -11,7 +16,11 @@ use control_core::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uom::{
|
||||
ConstZero,
|
||||
si::f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
|
||||
si::{
|
||||
f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
|
||||
length::{meter, millimeter},
|
||||
time::second,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
@@ -43,6 +52,7 @@ pub struct PullerSpeedController {
|
||||
enabled: bool,
|
||||
pub target_speed: Velocity,
|
||||
pub target_diameter: Length,
|
||||
pub measured_diameter: Length,
|
||||
pub regulation_mode: PullerRegulationMode,
|
||||
/// Forward rotation direction. If false, applies negative sign to speed
|
||||
pub forward: bool,
|
||||
@@ -53,6 +63,7 @@ pub struct PullerSpeedController {
|
||||
/// Converter for linear to angular transformations
|
||||
pub converter: LinearStepConverter,
|
||||
pub last_speed: Velocity,
|
||||
pub p_dead_controller: TimeAgnosticDeadTimePiController,
|
||||
}
|
||||
|
||||
impl PullerSpeedController {
|
||||
@@ -69,6 +80,7 @@ impl PullerSpeedController {
|
||||
enabled: false,
|
||||
target_speed,
|
||||
target_diameter,
|
||||
measured_diameter: Length::ZERO,
|
||||
regulation_mode: PullerRegulationMode::Speed,
|
||||
forward: true,
|
||||
gear_ratio: GearRatio::default(),
|
||||
@@ -79,6 +91,11 @@ impl PullerSpeedController {
|
||||
),
|
||||
converter,
|
||||
last_speed: Velocity::ZERO,
|
||||
p_dead_controller: TimeAgnosticDeadTimePiController::new(
|
||||
0.1 / 10000.0,
|
||||
0.0,
|
||||
Duration::ZERO,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +111,10 @@ impl PullerSpeedController {
|
||||
self.target_diameter = target;
|
||||
}
|
||||
|
||||
pub fn set_measured_diameter(&mut self, target: f64) {
|
||||
self.measured_diameter = Length::new::<millimeter>(target);
|
||||
}
|
||||
|
||||
pub const fn set_regulation_mode(&mut self, regulation: PullerRegulationMode) {
|
||||
self.regulation_mode = regulation;
|
||||
}
|
||||
@@ -111,12 +132,27 @@ impl PullerSpeedController {
|
||||
}
|
||||
|
||||
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,
|
||||
if !self.enabled {
|
||||
return Velocity::ZERO;
|
||||
}
|
||||
|
||||
let base_speed = match self.regulation_mode {
|
||||
PullerRegulationMode::Speed => {
|
||||
let directional_target = if self.forward {
|
||||
self.target_speed
|
||||
} else {
|
||||
-self.target_speed
|
||||
};
|
||||
self.acceleration_controller.update(directional_target, t)
|
||||
}
|
||||
PullerRegulationMode::Diameter => {
|
||||
let diameter_speed = self.speed_from_diameter(t);
|
||||
if self.forward {
|
||||
diameter_speed
|
||||
} else {
|
||||
-diameter_speed
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply gear ratio multiplier
|
||||
@@ -130,6 +166,49 @@ impl PullerSpeedController {
|
||||
speed
|
||||
}
|
||||
|
||||
fn speed_from_diameter(&mut self, now: Instant) -> Velocity {
|
||||
// get current error
|
||||
let error =
|
||||
self.measured_diameter.get::<millimeter>() - self.target_diameter.get::<millimeter>();
|
||||
|
||||
// calculate/set deadtime based of speed and distance
|
||||
let deadtime = Self::calc_deadtime(self.last_speed, Length::new::<meter>(2.0));
|
||||
self.p_dead_controller.set_dead(deadtime);
|
||||
|
||||
// get speed change from p controller
|
||||
let speed_change = self.p_dead_controller.update(error, now);
|
||||
|
||||
// apply speed change to target speed
|
||||
let next_speed = self.last_speed + Velocity::new::<meter_per_minute>(speed_change);
|
||||
|
||||
// clamp the speed to 0 - 50 for safety
|
||||
Self::clamp_speed(
|
||||
next_speed,
|
||||
Velocity::new::<meter_per_minute>(0.0),
|
||||
Velocity::new::<meter_per_minute>(50.0),
|
||||
)
|
||||
}
|
||||
|
||||
fn calc_deadtime(speed: Velocity, distance: Length) -> Duration {
|
||||
// check if speed is 0 => This should not happen
|
||||
if speed <= Velocity::ZERO {
|
||||
return Duration::from_secs(180);
|
||||
}
|
||||
// get the duration based of distance and speed
|
||||
let secs = (distance / speed).get::<second>().max(0.0);
|
||||
Duration::from_secs_f64(secs)
|
||||
}
|
||||
|
||||
fn clamp_speed(speed: Velocity, min: Velocity, max: Velocity) -> Velocity {
|
||||
if speed < min {
|
||||
min
|
||||
} else if speed > max {
|
||||
max
|
||||
} else {
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user