Compare commits

...
46 Commits
Author SHA1 Message Date
just-some-entity a9ea77c356 enable spool length task 2026-03-01 09:36:53 +01:00
just-some-entity c0c915e731 update spool 2026-03-01 08:29:03 +01:00
just-some-entity 14ece96c44 finalize (hopefully) traverse 2026-03-01 07:53:50 +01:00
just-some-entity 7f5df18c61 finalize puller before connection 2026-03-01 06:09:07 +01:00
just-some-entity fadf00e8e4 update puller 2026-03-01 05:45:56 +01:00
just-some-entity 9867948c26 update puller 2026-03-01 05:07:03 +01:00
just-some-entity 2a6d382b92 idk 2026-03-01 04:06:45 +01:00
just-some-entity a577930bd9 enable updates partially 2026-03-01 04:02:24 +01:00
just-some-entity ce39be052e disable updates to check for performance degradation 2026-03-01 03:52:36 +01:00
just-some-entity 61f807b36a fix stuff 2026-03-01 03:32:18 +01:00
just-some-entity 3f15dc5179 fix enum 2026-03-01 03:30:01 +01:00
just-some-entity 75343fdf24 update UI 2026-03-01 03:20:44 +01:00
just-some-entity 0dfc4f1df6 fix schema issue 2026-03-01 02:43:14 +01:00
just-some-entity d43730bd5f refactor frontend 2026-03-01 02:33:40 +01:00
just-some-entity 0b8375d543 fix unstable issue 2026-02-28 23:32:04 +01:00
just-some-entity 751a8d26c9 update refactor 2026-02-28 23:20:51 +01:00
just-some-entity d5d4dfae7f implement missing mutations 2026-02-28 18:04:41 +01:00
just-some-entity 4b950b2865 remove regulation 2026-02-28 17:29:05 +01:00
just-some-entity 7d330ffee3 remove ui element 2026-02-28 17:23:49 +01:00
just-some-entity b0b4e2732b update electron event names 2026-02-28 16:44:28 +01:00
jsentity b54fb95cd4 fix main.rs GenericEvent import 2026-02-28 15:29:29 +01:00
jsentity 5bace31ab0 remove progress.txt 2026-02-28 15:18:58 +01:00
jsentity 274cff4494 finish rough refactor 2026-02-28 15:18:27 +01:00
jsentity 24c9a8f9ac refactor update 2026-02-28 12:48:47 +01:00
jsentity d5aeb6f751 create task 2026-02-27 20:38:45 +01:00
jsentity 8c09287a0e refactor update 2026-02-27 19:51:02 +01:00
jsentity 520148b149 refactor winder2 to make interacting with it easier 2026-02-27 04:22:10 +01:00
just-some-entity 2f270e17b4 oh hey look, more changes 2026-02-25 20:56:44 +01:00
just-some-entity 3b3f04d307 update or smth 2026-02-25 20:24:59 +01:00
just-some-entity 2ffb3e5dfa mid progress update 2026-02-25 19:44:40 +01:00
just-some-entity c5b855e9aa fix formatting 2026-02-24 21:58:07 +01:00
just-some-entity 8bb77d2a34 remove log statements to prevent lag 2026-02-24 21:29:47 +01:00
just-some-entity 4a4e57663a re-add machine filter for winder ui 2026-02-24 21:15:43 +01:00
just-some-entity 1355aea687 remove process exit 2026-02-24 21:07:37 +01:00
just-some-entity 9ea05f94e2 implement twoway request so laser can get hold of winder and send requests to it 2026-02-24 20:58:30 +01:00
just-some-entity a119a89835 oh hey look, more exists hooray 2026-02-24 20:39:28 +01:00
just-some-entity 1dd9c5f263 stop filtering machines for selection 2026-02-24 20:24:48 +01:00
just-some-entity e99a94c6e8 terminate program if message received. Idk how else to test this soo... 2026-02-24 19:36:48 +01:00
just-some-entity 7d6b4e1446 update logging 2026-02-24 17:30:15 +01:00
just-some-entity 22b6b464f5 add logs for testing 2026-02-24 17:12:35 +01:00
just-some-entity 257c5c453e fix build issues (hopefully) 2026-02-24 17:02:23 +01:00
just-some-entity 8303c091f7 fix formatting 2026-02-24 16:35:07 +01:00
just-some-entity 6030b5a795 cleanup and add simple inline docs 2026-02-24 16:31:48 +01:00
just-some-entity 60b016229f compute live values only once 2026-02-24 16:27:56 +01:00
just-some-entity 15e6db0c1b remove todo()! to avoid potential panic 2026-02-24 16:06:58 +01:00
just-some-entity d900195566 Implement the Connection between the Laser and the Winder. 2026-02-24 15:59:12 +01:00
61 changed files with 3930 additions and 3988 deletions

No files matched your search

@@ -0,0 +1,112 @@
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import React from "react";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
interface MachineIdentificationUnique {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}
interface Machine {
name: string;
machine_identification_unique: MachineIdentificationUnique;
}
interface MachineSelectorProps {
machines: Machine[];
selectedMachine?: Machine | null;
connectedMachineState?: {
machine_identification_unique?: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
} | null;
is_available: boolean;
};
setConnectedMachine: (machine: MachineIdentificationUnique) => void;
clearConnectedMachine?: () => void;
title?: string;
}
export const MachineSelector: React.FC<MachineSelectorProps> = ({
machines,
selectedMachine,
connectedMachineState,
setConnectedMachine,
clearConnectedMachine,
}) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded border px-4 py-2 text-left">
<Icon name="lu:Settings" className="text-xl" />
<span>
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex min-h-[48px] cursor-pointer items-center gap-2 py-2${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Power" className="text-lg text-green-600" />
) : (
<Icon name="lu:Power" className="text-lg text-gray-400" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:Settings" className="text-lg" />
<span>Clear Selection</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -15,7 +15,7 @@ import { StatusBadge } from "@/control/StatusBadge";
import { useWinder2 } from "./useWinder";
import {
Mode,
SpoolAutomaticActionMode,
OnSpoolLengthTaskCompletedAction,
getGearRatioMultiplier,
} from "./winder2Namespace";
import { TensionArm } from "../TensionArm";
@@ -29,6 +29,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { getWinder2TraverseMax } from "./winder2Config";
import { MachineSelector } from "../MachineSelector";
export function Winder2ControlPage() {
const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false);
@@ -38,46 +39,65 @@ export function Winder2ControlPage() {
const {
state,
defaultState,
enableTraverseLaserpointer,
tensionArmAngle,
zeroTensionArmAngle,
spoolRpm,
setMode,
pullerSpeed,
spoolProgress,
setPullerRegulationMode,
setPullerTargetSpeed,
traversePosition,
resetSpoolProgress,
selectedMachine,
filteredMachines,
isLoading,
isDisabled,
// setters
setMode,
setSpoolDirection,
setSpoolSpeedControlMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
setPullerDirection,
setPullerGearRatio,
setPullerSpeedControlMode,
setPullerFixedTargetSpeed,
setPullerAdaptiveBaseSpeed,
setPullerAdaptiveDeviationMax,
setPullerAdaptiveReferenceMachine,
setTraverseLimitInner,
setTraverseLimitOuter,
setTraverseStepSize,
setTraversePadding,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
isLoading,
isDisabled,
setTraverseLaserpointerEnabled,
calibrateTensionArmAngle,
setSpoolLengthTaskTargetLength,
resetSpoolLengthTaskProgress,
setOnSpoolLengthTaskCompletedAction,
} = useWinder2();
// Calculate max speed based on gear ratio
const gearRatioMultiplier = getGearRatioMultiplier(
state?.puller_state?.gear_ratio,
);
const maxMotorSpeed = 75; // Maximum motor speed in m/min
const maxTargetSpeed = maxMotorSpeed / gearRatioMultiplier;
const maxMotorSpeed = 50; // Maximum motor speed in m/min
const maxPullerSpeed = maxMotorSpeed / gearRatioMultiplier;
const handleResetProgress = () => {
// Check if the machine is currently in Wind mode
if (state?.mode_state?.mode === "Wind") {
if (state?.mode === "Wind") {
setShowResetConfirmDialog(true);
} else {
resetSpoolProgress();
resetSpoolLengthTaskProgress();
}
};
const confirmResetProgress = () => {
resetSpoolProgress();
resetSpoolLengthTaskProgress();
setShowResetConfirmDialog(false);
};
@@ -131,7 +151,7 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:ArrowLeftToLine"
onClick={gotoTraverseLimitOuter}
disabled={isDisabled}
disabled={isDisabled || state?.traverse_state?.can_go_out}
isLoading={isLoading}
>
Go to Outer Limit
@@ -162,7 +182,7 @@ export function Winder2ControlPage() {
icon="lu:ArrowRightToLine"
onClick={gotoTraverseLimitInner}
disabled={isDisabled}
isLoading={isLoading}
isLoading={isLoading || state?.traverse_state?.can_go_in}
>
Go to Inner Limit
</TouchButton>
@@ -170,12 +190,12 @@ export function Winder2ControlPage() {
</div>
<Label label="Laserpointer">
<SelectionGroupBoolean
value={state?.traverse_state.laserpointer}
value={state?.traverse_state.laserpointer_enabled}
disabled={isLoading || isDisabled}
loading={isLoading}
optionFalse={{ children: "Off", icon: "lu:LightbulbOff" }}
optionTrue={{ children: "On", icon: "lu:Lightbulb" }}
onChange={enableTraverseLaserpointer}
onChange={setTraverseLaserpointerEnabled}
/>
</Label>
<Label label="Home">
@@ -183,7 +203,7 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:House"
onClick={() => gotoTraverseHome()}
disabled={isDisabled}
disabled={isDisabled || state?.traverse_state?.can_go_home}
isLoading={isLoading}
>
Go to Home
@@ -205,20 +225,20 @@ export function Winder2ControlPage() {
<TouchButton
variant="outline"
icon="lu:House"
onClick={zeroTensionArmAngle}
onClick={calibrateTensionArmAngle}
disabled={isDisabled}
isLoading={isLoading}
>
Set Zero Point
Calibrate
</TouchButton>
{!state?.tension_arm_state?.zeroed && (
<StatusBadge variant="error">Not Zeroed</StatusBadge>
{!state?.tension_arm_state?.is_calibrated && (
<StatusBadge variant="error">Not Calibrated</StatusBadge>
)}
</ControlCard>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<Mode>
value={state?.mode_state.mode}
value={state?.mode}
disabled={isDisabled}
loading={isLoading}
onChange={setMode}
@@ -247,7 +267,7 @@ export function Winder2ControlPage() {
children: "Wind",
icon: "lu:RefreshCcw",
isActiveClassName: "bg-green-600",
disabled: !state?.mode_state?.can_wind,
disabled: !state?.can_wind,
className: "h-full",
},
}}
@@ -261,38 +281,98 @@ export function Winder2ControlPage() {
timeseries={pullerSpeed}
renderValue={(value) => roundToDecimals(value, 1)}
/>
<Label label="Regulation">
<Label label="Speed Regulation">
<SelectionGroup
value={state?.puller_state?.regulation}
options={{
Speed: {
children: "Speed",
icon: "lu:Gauge",
},
Diameter: {
children: "Diameter",
icon: "lu:Sun",
disabled: true,
},
}}
onChange={setPullerRegulationMode}
value={state?.puller_state?.speed_control_mode}
disabled={isDisabled}
loading={isLoading}
options={{
Fixed: {
children: "Fixed",
icon: "lu:Crosshair",
},
Adaptive: {
children: "Adaptive",
icon: "lu:Brain",
},
}}
onChange={(value) =>
setPullerSpeedControlMode(value)
}
/>
</Label>
<Label label="Target Speed">
<EditValue
value={state?.puller_state?.target_speed}
unit="m/min"
title="Target Speed"
defaultValue={defaultState?.puller_state?.target_speed}
min={0}
max={maxTargetSpeed}
step={0.1}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setPullerTargetSpeed}
/>
</Label>
{state?.puller_state?.speed_control_mode ===
"Fixed" && (
<>
<Label label="Target Speed">
<EditValue
value={state?.puller_state?.fixed_target_speed}
title={"Target Speed"}
unit="m/min"
step={0.1}
min={0}
max={maxPullerSpeed}
defaultValue={
defaultState?.puller_state?.fixed_target_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setPullerFixedTargetSpeed(value)}
/>
</Label>
</>
)}
{state?.puller_state?.speed_control_mode ===
"Adaptive" && (
<>
<Label label="Base Speed">
<EditValue
value={state?.puller_state?.adaptive_base_speed}
title={"Base Speed"}
unit="m/min"
step={0.1}
min={0}
max={maxPullerSpeed}
defaultValue={
defaultState?.puller_state?.adaptive_base_speed
}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) => setPullerAdaptiveBaseSpeed(value)}
/>
</Label>
<Label label="Max Deviation">
<EditValue
value={state?.puller_state?.adaptive_deviation_max}
title={"Max Deviation"}
unit="m/min"
step={10}
min={0.1}
max={maxPullerSpeed}
defaultValue={
defaultState?.puller_state?.adaptive_deviation_max
}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) => setPullerAdaptiveDeviationMax(value)}
/>
</Label>
<Label label="Reference Machine">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.puller_state.adaptive_reference_machine}
setConnectedMachine={(machine) => {
setPullerAdaptiveReferenceMachine(machine);
}}
clearConnectedMachine={() =>
{
if (!selectedMachine) return;
setPullerAdaptiveReferenceMachine(null);
}}
/>
</Label>
</>
)}
</ControlCard>
<ControlCard className="bg-red" title="Spool Autostop">
@@ -305,7 +385,7 @@ export function Winder2ControlPage() {
<Label label="Target Length">
<EditValue
value={state?.spool_automatic_action_state.spool_required_meters}
value={state?.spool_length_task_state.target_length}
unit="m"
title="Expected Meters"
defaultValue={250}
@@ -313,7 +393,7 @@ export function Winder2ControlPage() {
max={10000}
step={10}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={setSpoolAutomaticRequiredMeters}
onChange={setSpoolLengthTaskTargetLength}
/>
</Label>
@@ -327,13 +407,13 @@ export function Winder2ControlPage() {
</TouchButton>
<Label label="After Target Length Reached">
<SelectionGroup<SpoolAutomaticActionMode>
<SelectionGroup<OnSpoolLengthTaskCompletedAction>
value={
state?.spool_automatic_action_state.spool_automatic_action_mode
state?.spool_length_task_state.on_completed_action
}
disabled={isDisabled}
loading={isLoading}
onChange={setSpoolAutomaticAction}
onChange={setOnSpoolLengthTaskCompletedAction}
orientation="vertical"
options={{
Hold: {
@@ -19,7 +19,6 @@ export function Winder2GraphsPage() {
traversePosition,
tensionArmAngle,
pullerSpeed,
targetPullerSpeed,
spoolProgress,
} = useWinder2();
@@ -32,8 +31,6 @@ export function Winder2GraphsPage() {
<PullerSpeedGraph
syncHook={syncHook}
newData={pullerSpeed}
targetSeries={targetPullerSpeed}
targetSpeed={state?.puller_state?.target_speed}
unit="m/min"
renderValue={(value) => roundToDecimals(value, 0)}
/>
@@ -245,30 +242,16 @@ export function SpoolProgressGraph({
export function PullerSpeedGraph({
syncHook,
newData,
targetSeries,
targetSpeed,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
targetSeries?: TimeSeries;
targetSpeed?: number;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const lines: GraphLine[] = [];
if (targetSpeed !== undefined) {
lines.push({
type: "target",
value: targetSpeed,
targetSeries,
label: "Target Speed",
color: "#06b6d4", // Match series color
});
}
const config: GraphConfig = {
title: "Puller Speed",
icon: "lu:Gauge",
@@ -6,7 +6,7 @@ import { PresetsPage } from "@/components/preset/PresetsPage";
import { Preset } from "@/lib/preset/preset";
import {
pullerStateSchema,
spoolSpeedControllerStateSchema,
spoolStateSchema,
} from "./winder2Namespace";
import { z } from "zod";
import {
@@ -26,7 +26,7 @@ const winder2PresetDataSchema = z
})
.partial(),
puller_state: pullerStateSchema.partial(),
spool_speed_controller_state: spoolSpeedControllerStateSchema.partial(),
spool_speed_controller_state: spoolStateSchema.partial(),
})
.partial();
@@ -243,7 +243,7 @@ export function Winder2PresetsPage() {
laserpointer: s?.traverse_state?.laserpointer,
},
puller_state: s?.puller_state ?? {},
spool_speed_controller_state: s?.spool_speed_controller_state ?? {},
spool_speed_controller_state: s?.spool_state ?? {},
});
return (
@@ -22,28 +22,38 @@ export function Winder2SettingPage() {
const {
state,
defaultState,
setTraverseStepSize,
setTraversePadding,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseHome,
setPullerForward,
setPullerGearRatio,
setSpoolRegulationMode,
isLoading,
isDisabled,
// setters
setMode,
setSpoolDirection,
setSpoolSpeedControlMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolForward,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
isLoading,
isDisabled,
selectedMachine,
filteredMachines,
setConnectedMachine,
disconnectMachine,
setPullerDirection,
setPullerGearRatio,
setPullerSpeedControlMode,
setPullerFixedTargetSpeed,
setPullerAdaptiveBaseSpeed,
setPullerAdaptiveDeviationMax,
setPullerAdaptiveReferenceMachine,
setTraverseLimitInner,
setTraverseLimitOuter,
setTraverseStepSize,
setTraversePadding,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setTraverseLaserpointerEnabled,
calibrateTensionArmAngle,
setSpoolLengthTaskTargetLength,
resetSpoolLengthTaskProgress,
setOnSpoolLengthTaskCompletedAction,
} = useWinder2();
const handleXlModeChange = (enabled: boolean) => {
@@ -129,7 +139,7 @@ export function Winder2SettingPage() {
<ControlCard title="Spool">
<Label label="Speed Algorithm">
<SelectionGroup
value={state?.spool_speed_controller_state?.regulation_mode}
value={state?.spool_state?.speed_control_mode}
disabled={isDisabled}
loading={isLoading}
options={{
@@ -143,14 +153,14 @@ export function Winder2SettingPage() {
},
}}
onChange={(value) =>
setSpoolRegulationMode(value as "Adaptive" | "MinMax")
setSpoolSpeedControlMode(value as "Adaptive" | "MinMax")
}
/>
</Label>
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.spool_speed_controller_state?.forward}
value={state?.spool_state?.direction == "Forward"}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
@@ -161,23 +171,23 @@ export function Winder2SettingPage() {
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setSpoolForward(value)}
onChange={(value) => setSpoolDirection(value ? "Forward" : "Reverse")}
/>
</Label>
{state?.spool_speed_controller_state?.regulation_mode ===
{state?.spool_state?.speed_control_mode ===
"MinMax" && (
<>
<Label label="Minimum Speed">
<EditValue
value={state?.spool_speed_controller_state?.minmax_min_speed}
value={state?.spool_state?.minmax_min_speed}
title={"Minimum Speed"}
unit="rpm"
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_speed_controller_state?.minmax_min_speed
defaultState?.spool_state?.minmax_min_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMinSpeed(value)}
@@ -185,14 +195,14 @@ export function Winder2SettingPage() {
</Label>
<Label label="Maximum Speed">
<EditValue
value={state?.spool_speed_controller_state?.minmax_max_speed}
value={state?.spool_state?.minmax_max_speed}
title={"Maximum Speed"}
unit="rpm"
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_speed_controller_state?.minmax_max_speed
defaultState?.spool_state?.minmax_max_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMaxSpeed(value)}
@@ -201,13 +211,13 @@ export function Winder2SettingPage() {
</>
)}
{state?.spool_speed_controller_state?.regulation_mode ===
{state?.spool_state?.speed_control_mode ===
"Adaptive" && (
<div className="flex flex-row flex-wrap gap-4">
<Label label="Tension Target">
<EditValue
value={
state?.spool_speed_controller_state?.adaptive_tension_target
state?.spool_state?.adaptive_tension_target
}
title={"Tension Target"}
unit={undefined}
@@ -215,7 +225,7 @@ export function Winder2SettingPage() {
min={0}
max={1}
defaultValue={
defaultState?.spool_speed_controller_state
defaultState?.spool_state
?.adaptive_tension_target
}
renderValue={(value) => roundToDecimals(value, 2)}
@@ -225,7 +235,7 @@ export function Winder2SettingPage() {
<Label label="Learning Rate">
<EditValue
value={
state?.spool_speed_controller_state
state?.spool_state
?.adaptive_radius_learning_rate
}
title={"Radius Learning Rate"}
@@ -234,7 +244,7 @@ export function Winder2SettingPage() {
min={0}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
defaultState?.spool_state
?.adaptive_radius_learning_rate
}
renderValue={(value) => roundToDecimals(value, 2)}
@@ -246,7 +256,7 @@ export function Winder2SettingPage() {
<Label label="Max Speed Multiplier">
<EditValue
value={
state?.spool_speed_controller_state
state?.spool_state
?.adaptive_max_speed_multiplier
}
title={"Max Speed Multiplier"}
@@ -255,7 +265,7 @@ export function Winder2SettingPage() {
min={0.1}
max={10}
defaultValue={
defaultState?.spool_speed_controller_state
defaultState?.spool_state
?.adaptive_max_speed_multiplier
}
renderValue={(value) => roundToDecimals(value, 1)}
@@ -267,7 +277,7 @@ export function Winder2SettingPage() {
<Label label="Acceleration Factor">
<EditValue
value={
state?.spool_speed_controller_state
state?.spool_state
?.adaptive_acceleration_factor
}
title={"Acceleration Factor"}
@@ -276,7 +286,7 @@ export function Winder2SettingPage() {
min={0.01}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
defaultState?.spool_state
?.adaptive_acceleration_factor
}
renderValue={(value) => roundToDecimals(value, 2)}
@@ -288,7 +298,7 @@ export function Winder2SettingPage() {
<Label label="Deaccel. Urgency">
<EditValue
value={
state?.spool_speed_controller_state
state?.spool_state
?.adaptive_deacceleration_urgency_multiplier
}
title={"Deacceleration Urgency Multiplier"}
@@ -297,7 +307,7 @@ export function Winder2SettingPage() {
min={1}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
defaultState?.spool_state
?.adaptive_deacceleration_urgency_multiplier
}
renderValue={(value) => roundToDecimals(value, 1)}
@@ -313,7 +323,7 @@ export function Winder2SettingPage() {
<ControlCard title="Puller">
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.puller_state?.forward}
value={state?.puller_state?.direction == "Forward"}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
@@ -324,7 +334,7 @@ export function Winder2SettingPage() {
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setPullerForward(value)}
onChange={(value) => setPullerDirection(value ? "Forward" : "Reverse")}
/>
</Label>
<Label label="Gear Ratio">
@@ -337,37 +347,23 @@ export function Winder2SettingPage() {
children: "1:1",
icon: "lu:Circle",
},
OneToFive: {
children: "1:5",
FiveToOne: {
children: "5:1",
icon: "lu:Cog",
},
OneToTen: {
children: "1:10",
TenToOne: {
children: "10:1",
icon: "lu:Cog",
},
}}
onChange={(value) =>
setPullerGearRatio(
value as "OneToOne" | "OneToFive" | "OneToTen",
value as "OneToOne" | "FiveToOne" | "TenToOne",
)
}
/>
</Label>
</ControlCard>
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_machine_state}
setConnectedMachine={setConnectedMachine}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectMachine(selectedMachine.machine_identification_unique);
}}
/>
</ControlGrid>
</Page>
);
+418 -396
View File
@@ -9,18 +9,20 @@ import { VENDOR_QITECH, winder2 } from "@/machines/properties";
import { winder2SerialRoute } from "@/routes/routes";
import { z } from "zod";
import {
SpoolRegulationMode,
SpoolSpeedControlMode,
StateEvent,
useWinder2Namespace,
modeSchema,
Mode,
spoolRegulationModeSchema,
pullerRegulationSchema,
PullerRegulation,
SpoolAutomaticActionMode,
spoolAutomaticActionModeSchema,
spoolSpeedControlModeSchema,
OnSpoolLengthTaskCompletedAction,
spoolLengthTaskCompletedAction,
gearRatioSchema,
GearRatio,
pullerSpeedControlModeSchema,
directionSchema,
Direction,
PullerSpeedControlMode
} from "./winder2Namespace";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
@@ -62,10 +64,9 @@ export function useWinder2() {
defaultState,
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
spoolLengthTaskProgress,
} = useWinder2Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -78,25 +79,76 @@ export function useWinder2() {
}
}, [state]);
// Request functions for all operations
const { request: requestTraverseGotoLimitInner } = useMachineMutation(
z.literal("GotoTraverseLimitInner"),
);
const { request: requestTraverseGotoLimitOuter } = useMachineMutation(
z.literal("GotoTraverseLimitOuter"),
);
const { request: requestTraverseGotoHome } = useMachineMutation(
z.literal("GotoTraverseHome"),
);
const { request: requestSetLaserpointer } = useMachineMutation(
z.object({ EnableTraverseLaserpointer: z.boolean() }),
);
// boilerplate junk ends here..
// machine request functions
const { request: requestModeSet } = useMachineMutation(
z.object({ SetMode: modeSchema }),
);
const { request: requestTensionArmZero } = useMachineMutation(
z.literal("ZeroTensionArmAngle"),
// spool request functions
const { request: requestSpoolSetDirection } = useMachineMutation(
z.object({ SetSpoolDirection: directionSchema }),
);
const { request: requestSpoolSetSpeedControlMode } = useMachineMutation(
z.object({ SetSpoolSpeedControlMode: spoolSpeedControlModeSchema }),
);
const { request: requestSpoolSetMinMaxMinSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMinSpeed: z.number() }),
);
const { request: requestSpoolSetMinMaxMaxSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMaxSpeed: z.number() }),
);
const { request: requestSpoolSetAdaptiveTensionTarget } = useMachineMutation(
z.object({ SetSpoolAdaptiveTensionTarget: z.number() }),
);
const { request: requestSpoolSetAdaptiveRadiusLearningRate } = useMachineMutation(
z.object({ SetSpoolAdaptiveRadiusLearningRate: z.number() }),
);
const { request: requestSpoolSetAdaptiveMaxSpeedMultiplier } = useMachineMutation(
z.object({ SetSpoolAdaptiveMaxSpeedMultiplier: z.number() }),
);
const { request: requestSpoolSetAdaptiveAccelerationFactor } = useMachineMutation(
z.object({ SetSpoolAdaptiveAccelerationFactor: z.number() }),
);
const { request: requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier } = useMachineMutation(
z.object({ SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: z.number() }),
);
// puller request functions
const { request: requestPullerSetDirection } = useMachineMutation(
z.object({ SetPullerDirection: directionSchema }),
);
const { request: requestPullerSetGearRatio } = useMachineMutation(
z.object({ SetPullerGearRatio: gearRatioSchema }),
);
const { request: requestPullerSetSpeedControlMode } = useMachineMutation(
z.object({
SetPullerSpeedControlMode: pullerSpeedControlModeSchema,
}),
);
const { request: requestPullerSetFixedTargetSpeed } = useMachineMutation(
z.object({
SetPullerFixedTargetSpeed: z.number(),
}),
);
const { request: requestPullerSetAdaptiveBaseSpeed } = useMachineMutation(
z.object({
SetPullerAdaptiveBaseSpeed: z.number(),
}),
);
const { request: requestPullerSetAdaptiveDeviationMax } = useMachineMutation(
z.object({
SetPullerAdaptiveDeviationMax: z.number(),
}),
);
const { request: requestPullerSetAdaptiveReferenceMachine } = useMachineMutation(
z.object({
SetPullerAdaptiveReferenceMachine: machineIdentificationUnique.nullable(),
}),
);
// traverse
const { request: requestTraverseSetLimitInner } = useMachineMutation(
z.object({ SetTraverseLimitInner: z.number() }),
);
@@ -109,75 +161,33 @@ export function useWinder2() {
const { request: requestTraverseSetPadding } = useMachineMutation(
z.object({ SetTraversePadding: z.number() }),
);
const { request: requestPullerSetTargetSpeed } = useMachineMutation(
z.object({ SetPullerTargetSpeed: z.number() }),
const { request: requestTraverseGotoLimitInner } = useMachineMutation(
z.literal("GotoTraverseLimitInner"),
);
const { request: requestPullerSetTargetDiameter } = useMachineMutation(
z.object({ SetPullerTargetDiameter: z.number() }),
const { request: requestTraverseGotoLimitOuter } = useMachineMutation(
z.literal("GotoTraverseLimitOuter"),
);
const { request: requestPullerSetRegulationMode } = useMachineMutation(
z.object({
SetPullerRegulationMode: pullerRegulationSchema,
}),
const { request: requestTraverseGotoHome } = useMachineMutation(
z.literal("GotoTraverseHome"),
);
const { request: requestPullerSetForward } = useMachineMutation(
z.object({ SetPullerForward: z.boolean() }),
);
const { request: requestPullerSetGearRatio } = useMachineMutation(
z.object({ SetPullerGearRatio: gearRatioSchema }),
);
const { request: requestSpoolSetRegulationMode } = useMachineMutation(
z.object({ SetSpoolRegulationMode: spoolRegulationModeSchema }),
);
const { request: requestSpoolSetMinMaxMinSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMinSpeed: z.number() }),
);
const { request: requestSpoolSetMinMaxMaxSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMaxSpeed: z.number() }),
);
const { request: requestSpoolSetForward } = useMachineMutation(
z.object({ SetSpoolForward: z.boolean() }),
);
const { request: requestSpoolSetAdaptiveTensionTarget } = useMachineMutation(
z.object({ SetSpoolAdaptiveTensionTarget: z.number() }),
);
const { request: requestSpoolSetAdaptiveRadiusLearningRate } =
useMachineMutation(
z.object({ SetSpoolAdaptiveRadiusLearningRate: z.number() }),
);
const { request: requestSpoolSetAdaptiveMaxSpeedMultiplier } =
useMachineMutation(
z.object({ SetSpoolAdaptiveMaxSpeedMultiplier: z.number() }),
);
const { request: requestSpoolSetAdaptiveAccelerationFactor } =
useMachineMutation(
z.object({ SetSpoolAdaptiveAccelerationFactor: z.number() }),
);
const { request: requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier } =
useMachineMutation(
z.object({ SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: z.number() }),
);
const { request: requestSpoolAutomaticRequiredMeters } = useMachineMutation(
z.object({ SetSpoolAutomaticRequiredMeters: z.number() }),
);
const { request: requestSpoolResetProgress } = useMachineMutation(
z.literal("ResetSpoolProgress"),
const { request: requestSetLaserpointerEnabled } = useMachineMutation(
z.object({ SetTraverseLaserpointerEnabled: z.boolean() }),
);
const { request: requestSpoolAutomaticAction } = useMachineMutation(
z.object({ SetSpoolAutomaticAction: spoolAutomaticActionModeSchema }),
// tension arm
const { request: requestTensionArmCalibrate } = useMachineMutation(
z.literal("CalibrateTensionArmAngle"),
);
const { request: requestConnectedMachine } = useMachineMutation(
z.object({
SetConnectedMachine: machineIdentificationUnique,
}),
// spool length task
const { request: requestSpoolLengthTaskSetTargetLength } = useMachineMutation(
z.object({ SetSpoolLengthTaskTargetLength: z.number() }),
);
const { request: requestDisconnectedMachine } = useMachineMutation(
z.object({
DisconnectMachine: machineIdentificationUnique,
}),
const { request: requestSpoolLengthTaskResetProgress } = useMachineMutation(
z.literal("ResetSetSpoolLengthTaskProgress"),
);
const { request: requestSpoolLengthTaskSetOnCompletedAction } = useMachineMutation(
z.object({ SetOnSpoolLengthTaskCompletedAction: spoolLengthTaskCompletedAction }),
);
// Helper function for optimistic updates using produce
@@ -193,19 +203,245 @@ export function useWinder2() {
};
// Action functions
const zeroTensionArmAngle = () => {
const setMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.tension_arm_state.zeroed = true;
current.data.mode = mode;
},
() =>
requestTensionArmZero({
requestModeSet({
machine_identification_unique: machineIdentification,
data: "ZeroTensionArmAngle",
data: { SetMode: mode },
}),
);
};
// spool
const setSpoolDirection = (direction: Direction) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.direction = direction;
},
() =>
requestSpoolSetDirection({
machine_identification_unique: machineIdentification,
data: { SetSpoolDirection: direction },
}),
);
};
const setSpoolSpeedControlMode = (mode: SpoolSpeedControlMode) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.speed_control_mode = mode;
},
() =>
requestSpoolSetSpeedControlMode({
machine_identification_unique: machineIdentification,
data: { SetSpoolSpeedControlMode: mode },
}),
);
};
const setSpoolMinMaxMinSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.minmax_min_speed = speed;
},
() =>
requestSpoolSetMinMaxMinSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMinSpeed: speed },
}),
);
};
const setSpoolMinMaxMaxSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.minmax_max_speed = speed;
},
() =>
requestSpoolSetMinMaxMaxSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMaxSpeed: speed },
}),
);
};
const setSpoolAdaptiveTensionTarget = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_tension_target =
value;
},
() =>
requestSpoolSetAdaptiveTensionTarget({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveTensionTarget: value },
}),
);
};
const setSpoolAdaptiveRadiusLearningRate = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_radius_learning_rate =
value;
},
() =>
requestSpoolSetAdaptiveRadiusLearningRate({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveRadiusLearningRate: value },
}),
);
};
const setSpoolAdaptiveMaxSpeedMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_max_speed_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveMaxSpeedMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveMaxSpeedMultiplier: value },
}),
);
};
const setSpoolAdaptiveAccelerationFactor = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_acceleration_factor =
value;
},
() =>
requestSpoolSetAdaptiveAccelerationFactor({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveAccelerationFactor: value },
}),
);
};
const setSpoolAdaptiveDeaccelerationUrgencyMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_deacceleration_urgency_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: value },
}),
);
};
// puller
const setPullerDirection = (direction: Direction) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.direction = direction;
},
() =>
requestPullerSetDirection({
machine_identification_unique: machineIdentification,
data: { SetPullerDirection: direction },
}),
);
};
const setPullerGearRatio = (gearRatio: GearRatio) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.gear_ratio = gearRatio;
},
() => {
requestPullerSetGearRatio({
machine_identification_unique: machineIdentification,
data: { SetPullerGearRatio: gearRatio },
});
},
);
};
const setPullerSpeedControlMode = (mode: PullerSpeedControlMode) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.speed_control_mode = mode;
},
() =>
requestPullerSetSpeedControlMode({
machine_identification_unique: machineIdentification,
data: { SetPullerSpeedControlMode: mode },
}),
);
};
const setPullerFixedTargetSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.fixed_target_speed = speed;
},
() =>
requestPullerSetFixedTargetSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerFixedTargetSpeed: speed },
}),
);
};
const setPullerAdaptiveBaseSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.fixed_target_speed = speed;
},
() =>
requestPullerSetAdaptiveBaseSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerAdaptiveBaseSpeed: speed },
}),
);
};
const setPullerAdaptiveDeviationMax = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.fixed_target_speed = speed;
},
() =>
requestPullerSetAdaptiveDeviationMax({
machine_identification_unique: machineIdentification,
data: { SetPullerAdaptiveDeviationMax: speed },
}),
);
};
const setPullerAdaptiveReferenceMachine = (machineUid: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
} | null) => {
updateStateOptimistically(
(current) => {
current.data.puller_state
.adaptive_reference_machine
.machine_identification_unique
= machineUid;
},
() =>
requestPullerSetAdaptiveReferenceMachine({
machine_identification_unique: machineIdentification,
data: { SetPullerAdaptiveReferenceMachine: machineUid },
}),
);
};
// traverse
const setTraverseLimitInner = (limitInner: number) => {
updateStateOptimistically(
(current) => {
@@ -232,6 +468,32 @@ export function useWinder2() {
);
};
const setTraverseStepSize = (stepSize: number) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.step_size = stepSize;
},
() =>
requestTraverseSetStepSize({
machine_identification_unique: machineIdentification,
data: { SetTraverseStepSize: stepSize },
}),
);
};
const setTraversePadding = (padding: number) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.padding = padding;
},
() =>
requestTraverseSetPadding({
machine_identification_unique: machineIdentification,
data: { SetTraversePadding: padding },
}),
);
};
const gotoTraverseLimitInner = () => {
updateStateOptimistically(
(current) => {
@@ -271,325 +533,64 @@ export function useWinder2() {
);
};
const enableTraverseLaserpointer = (enabled: boolean) => {
const setTraverseLaserpointerEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.laserpointer = enabled;
current.data.traverse_state.laserpointer_enabled = enabled;
},
() =>
requestSetLaserpointer({
requestSetLaserpointerEnabled({
machine_identification_unique: machineIdentification,
data: { EnableTraverseLaserpointer: enabled },
data: { SetTraverseLaserpointerEnabled: enabled },
}),
);
};
const setMode = (mode: Mode) => {
// tension arm
const calibrateTensionArmAngle = () => {
updateStateOptimistically(
(current) => {
current.data.mode_state.mode = mode;
current.data.tension_arm_state.is_calibrated = true;
},
() =>
requestModeSet({
requestTensionArmCalibrate({
machine_identification_unique: machineIdentification,
data: { SetMode: mode },
data: "CalibrateTensionArmAngle",
}),
);
};
const setTraverseStepSize = (stepSize: number) => {
// spool length task
const setSpoolLengthTaskTargetLength = (target_length: number) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.step_size = stepSize;
current.data.spool_length_task_state.target_length = target_length;
},
() =>
requestTraverseSetStepSize({
requestSpoolLengthTaskSetTargetLength({
machine_identification_unique: machineIdentification,
data: { SetTraverseStepSize: stepSize },
data: { SetSpoolLengthTaskTargetLength: target_length },
}),
);
};
const setTraversePadding = (padding: number) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.padding = padding;
},
() =>
requestTraverseSetPadding({
machine_identification_unique: machineIdentification,
data: { SetTraversePadding: padding },
}),
);
};
const setPullerTargetSpeed = (targetSpeed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.target_speed = targetSpeed;
},
() =>
requestPullerSetTargetSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerTargetSpeed: targetSpeed },
}),
);
};
const setPullerTargetDiameter = (targetDiameter: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.target_diameter = targetDiameter;
},
() =>
requestPullerSetTargetDiameter({
machine_identification_unique: machineIdentification,
data: { SetPullerTargetDiameter: targetDiameter },
}),
);
};
const setPullerRegulationMode = (regulationMode: PullerRegulation) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.regulation = regulationMode;
},
() =>
requestPullerSetRegulationMode({
machine_identification_unique: machineIdentification,
data: { SetPullerRegulationMode: regulationMode },
}),
);
};
const setPullerForward = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.forward = forward;
},
() =>
requestPullerSetForward({
machine_identification_unique: machineIdentification,
data: { SetPullerForward: forward },
}),
);
};
const setPullerGearRatio = (gearRatio: GearRatio) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.gear_ratio = gearRatio;
// Reset target speed to 0 to prevent sudden speed changes
current.data.puller_state.target_speed = 0;
},
async () => {
// First set the target speed to 0
await requestPullerSetTargetSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerTargetSpeed: 0 },
});
// Then set the gear ratio
await requestPullerSetGearRatio({
machine_identification_unique: machineIdentification,
data: { SetPullerGearRatio: gearRatio },
});
},
);
};
const setSpoolAutomaticRequiredMeters = (meters: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_automatic_action_state.spool_required_meters =
meters;
},
() =>
requestSpoolAutomaticRequiredMeters({
machine_identification_unique: machineIdentification,
data: { SetSpoolAutomaticRequiredMeters: meters },
}),
);
};
const setSpoolAutomaticAction = (mode: SpoolAutomaticActionMode) => {
updateStateOptimistically(
(current) => {
current.data.spool_automatic_action_state.spool_automatic_action_mode =
mode;
},
() =>
requestSpoolAutomaticAction({
machine_identification_unique: machineIdentification,
data: { SetSpoolAutomaticAction: mode },
}),
);
};
const resetSpoolProgress = () => {
requestSpoolResetProgress({
const resetSpoolLengthTaskProgress = () => {
requestSpoolLengthTaskResetProgress({
machine_identification_unique: machineIdentification,
data: "ResetSpoolProgress",
data: "ResetSetSpoolLengthTaskProgress",
});
};
const setSpoolRegulationMode = (mode: SpoolRegulationMode) => {
const setOnSpoolLengthTaskCompletedAction = (action: OnSpoolLengthTaskCompletedAction) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.regulation_mode = mode;
current.data.spool_length_task_state.on_completed_action =
action;
},
() =>
requestSpoolSetRegulationMode({
requestSpoolLengthTaskSetOnCompletedAction({
machine_identification_unique: machineIdentification,
data: { SetSpoolRegulationMode: mode },
}),
);
};
const setSpoolMinMaxMinSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.minmax_min_speed = speed;
},
() =>
requestSpoolSetMinMaxMinSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMinSpeed: speed },
}),
);
};
const setSpoolMinMaxMaxSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.minmax_max_speed = speed;
},
() =>
requestSpoolSetMinMaxMaxSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMaxSpeed: speed },
}),
);
};
const setSpoolForward = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.forward = forward;
},
() =>
requestSpoolSetForward({
machine_identification_unique: machineIdentification,
data: { SetSpoolForward: forward },
}),
);
};
const setSpoolAdaptiveTensionTarget = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_tension_target =
value;
},
() =>
requestSpoolSetAdaptiveTensionTarget({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveTensionTarget: value },
}),
);
};
const setSpoolAdaptiveRadiusLearningRate = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_radius_learning_rate =
value;
},
() =>
requestSpoolSetAdaptiveRadiusLearningRate({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveRadiusLearningRate: value },
}),
);
};
const setSpoolAdaptiveMaxSpeedMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_max_speed_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveMaxSpeedMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveMaxSpeedMultiplier: value },
}),
);
};
const setSpoolAdaptiveAccelerationFactor = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_acceleration_factor =
value;
},
() =>
requestSpoolSetAdaptiveAccelerationFactor({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveAccelerationFactor: value },
}),
);
};
const setSpoolAdaptiveDeaccelerationUrgencyMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_deacceleration_urgency_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: value },
}),
);
};
const setConnectedMachine = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_machine_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedMachine({
machine_identification_unique,
data: { SetConnectedMachine: machineIdentificationUnique },
}),
);
};
const disconnectMachine = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_machine_state.machine_identification_unique =
null;
},
() =>
requestDisconnectedMachine({
machine_identification_unique,
data: { DisconnectMachine: machineIdentificationUnique },
data: { SetOnSpoolLengthTaskCompletedAction: action },
}),
);
};
@@ -608,7 +609,7 @@ export function useWinder2() {
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0008,
0x0006, // id of laser machine
),
[machines, machineIdentification],
);
@@ -616,7 +617,7 @@ export function useWinder2() {
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.connected_machine_state?.machine_identification_unique
state?.data.puller_state.adaptive_reference_machine?.machine_identification_unique
?.serial;
return (
@@ -639,44 +640,65 @@ export function useWinder2() {
// Individual live values (TimeSeries)
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
spoolProgress: spoolLengthTaskProgress,
// Loading states
isLoading,
isDisabled,
// Action functions
enableTraverseLaserpointer,
//---------------------------------------------------------
// machine
setMode,
zeroTensionArmAngle,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
resetSpoolProgress,
setTraverseStepSize,
setTraversePadding,
setPullerTargetSpeed,
setPullerTargetDiameter,
setPullerRegulationMode,
setPullerForward,
setPullerGearRatio,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
setSpoolRegulationMode,
// spool
setSpoolDirection,
setSpoolSpeedControlMode,
// spool speed minmax strategy
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolForward,
// spool speed adaptive strategy
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
setConnectedMachine,
disconnectMachine,
// puller
setPullerDirection,
setPullerGearRatio,
setPullerSpeedControlMode,
// puller speed fixed strategy
setPullerFixedTargetSpeed,
// puller speed adaptive strategy
setPullerAdaptiveBaseSpeed,
setPullerAdaptiveDeviationMax,
setPullerAdaptiveReferenceMachine,
// traverse
setTraverseLimitInner,
setTraverseLimitOuter,
setTraverseStepSize,
setTraversePadding,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setTraverseLaserpointerEnabled,
// tension arm
calibrateTensionArmAngle,
// spool length task
setSpoolLengthTaskTargetLength,
resetSpoolLengthTaskProgress,
setOnSpoolLengthTaskCompletedAction,
};
}
@@ -28,38 +28,14 @@ export const liveValuesEventDataSchema = z.object({
puller_speed: z.number(),
spool_rpm: z.number(),
tension_arm_angle: z.number(),
spool_progress: z.number(),
spool_length_task_progress: z.number(),
});
/**
* Puller regulation type enum
* Direction enum
*/
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
/**
* Gear ratio enum for winding speed
*/
export const gearRatioSchema = z.enum(["OneToOne", "OneToFive", "OneToTen"]);
export type GearRatio = z.infer<typeof gearRatioSchema>;
/**
* Get the multiplier for a gear ratio
*/
export function getGearRatioMultiplier(
gearRatio: GearRatio | undefined,
): number {
switch (gearRatio) {
case "OneToOne":
return 1.0;
case "OneToFive":
return 5.0;
case "OneToTen":
return 10.0;
default:
return 1.0;
}
}
export const directionSchema = z.enum(["Forward", "Reverse"]);
export type Direction = z.infer<typeof directionSchema>;
/**
* Machine operation mode enum
@@ -67,74 +43,6 @@ export function getGearRatioMultiplier(
export const modeSchema = z.enum(["Standby", "Hold", "Pull", "Wind"]);
export type Mode = z.infer<typeof modeSchema>;
/**
* Machine operation mode enum
*/
export const spoolAutomaticActionModeSchema = z.enum([
"NoAction",
"Pull",
"Hold",
]);
export type SpoolAutomaticActionMode = z.infer<
typeof spoolAutomaticActionModeSchema
>;
/**
* Spool speed controller regulation mode enum
*/
export const spoolRegulationModeSchema = z.enum(["Adaptive", "MinMax"]);
export type SpoolRegulationMode = z.infer<typeof spoolRegulationModeSchema>;
export const spoolAutomaticActionStateSchema = z.object({
spool_required_meters: z.number(),
spool_automatic_action_mode: spoolAutomaticActionModeSchema,
});
export type SpoolAutomaticActionState = z.infer<
typeof spoolAutomaticActionStateSchema
>;
/**
* Traverse state schema
*/
export const traverseStateSchema = z.object({
limit_inner: z.number(),
limit_outer: z.number(),
position_in: z.number(),
position_out: z.number(),
is_going_in: z.boolean(),
is_going_out: z.boolean(),
is_homed: z.boolean(),
is_going_home: z.boolean(),
is_traversing: z.boolean(),
laserpointer: z.boolean(),
step_size: z.number(),
padding: z.number(),
can_go_in: z.boolean(),
can_go_out: z.boolean(),
can_go_home: z.boolean(),
});
/**
* Puller state schema
*/
export const pullerStateSchema = z.object({
regulation: pullerRegulationSchema,
target_speed: z.number(),
target_diameter: z.number(),
forward: z.boolean(),
gear_ratio: gearRatioSchema,
});
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
can_wind: z.boolean(),
});
/**
* Connected machine state scheme
*/
@@ -148,45 +56,147 @@ export const machineIdentificationUniqueSchema = z.object({
serial: z.number(),
});
export const connectedMachineStateSchema = z.object({
export const machineCrossConnectionStateSchema = z.object({
machine_identification_unique: machineIdentificationUniqueSchema.nullable(),
is_available: z.boolean(),
});
/**
* Gear ratio enum for winding speed
*/
export const gearRatioSchema = z.enum(["OneToOne", "FiveToOne", "TenToOne"]);
export type GearRatio = z.infer<typeof gearRatioSchema>;
/**
* Get the multiplier for a gear ratio
*/
export function getGearRatioMultiplier(
gearRatio: GearRatio | undefined,
): number {
switch (gearRatio) {
case "OneToOne":
return 1.0;
case "FiveToOne":
return 5.0;
case "TenToOne":
return 10.0;
default:
return 1.0;
}
}
/**
* Machine operation mode enum
*/
export const spoolLengthTaskCompletedAction = z.enum(["NoAction", "Pull", "Hold"]);
export type OnSpoolLengthTaskCompletedAction = z.infer<typeof spoolLengthTaskCompletedAction>;
/**
* Spool speed controller regulation mode enum
*/
export const spoolSpeedControlModeSchema = z.enum(["Adaptive", "MinMax"]);
export type SpoolSpeedControlMode = z.infer<typeof spoolSpeedControlModeSchema>;
/**
* Spool speed controller regulation mode enum
*/
export const pullerSpeedControlModeSchema = z.enum(["Fixed", "Adaptive"]);
export type PullerSpeedControlMode = z.infer<typeof pullerSpeedControlModeSchema>;
/**
* Spool speed controller state schema
*/
export const spoolStateSchema = z.object({
direction: directionSchema,
speed_control_mode: spoolSpeedControlModeSchema,
// min max mode
minmax_min_speed: z.number(),
minmax_max_speed: z.number(),
// adaptive mode
adaptive_tension_target: z.number(),
adaptive_radius_learning_rate: z.number(),
adaptive_max_speed_multiplier: z.number(),
adaptive_acceleration_factor: z.number(),
adaptive_deacceleration_urgency_multiplier: z.number(),
});
/**
* Puller state schema
*/
export const pullerStateSchema = z.object({
direction: directionSchema,
gear_ratio: gearRatioSchema,
speed_control_mode: pullerSpeedControlModeSchema,
fixed_target_speed: z.number(),
adaptive_base_speed: z.number(),
adaptive_deviation_max: z.number(),
adaptive_reference_machine: machineCrossConnectionStateSchema,
});
/**
* Traverse state schema
*/
export const traverseStateSchema = z.object({
// config
limit_inner: z.number(),
limit_outer: z.number(),
step_size: z.number(),
padding: z.number(),
// states
is_going_in: z.boolean(),
is_going_out: z.boolean(),
is_homed: z.boolean(),
is_going_home: z.boolean(),
is_traversing: z.boolean(),
// state transitions
can_go_in: z.boolean(),
can_go_out: z.boolean(),
can_go_home: z.boolean(),
// lazeeeeeeeer
laserpointer_enabled: z.boolean(),
});
/**
* Tension arm state schema
*/
export const tensionArmStateSchema = z.object({
zeroed: z.boolean(),
is_calibrated: z.boolean(),
});
/**
* Spool speed controller state schema
* spool length task state schema
*/
export const spoolSpeedControllerStateSchema = z.object({
regulation_mode: spoolRegulationModeSchema,
minmax_min_speed: z.number(),
minmax_max_speed: z.number(),
adaptive_tension_target: z.number(),
adaptive_radius_learning_rate: z.number(),
adaptive_max_speed_multiplier: z.number(),
adaptive_acceleration_factor: z.number(),
adaptive_deacceleration_urgency_multiplier: z.number(),
forward: z.boolean(),
export const spoolLengthTaskStateSchema = z.object({
target_length: z.number(),
on_completed_action: spoolLengthTaskCompletedAction,
});
export type SpoolLengthTaskState = z.infer<typeof spoolLengthTaskStateSchema>;
/**
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
// common
is_default_state: z.boolean(),
traverse_state: traverseStateSchema,
// machine
mode: modeSchema,
can_wind: z.boolean(),
// devices
spool_state: spoolStateSchema,
puller_state: pullerStateSchema,
mode_state: modeStateSchema,
traverse_state: traverseStateSchema,
tension_arm_state: tensionArmStateSchema,
spool_speed_controller_state: spoolSpeedControllerStateSchema,
spool_automatic_action_state: spoolAutomaticActionStateSchema,
connected_machine_state: connectedMachineStateSchema,
// tasks
spool_length_task_state: spoolLengthTaskStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -206,12 +216,11 @@ export type Winder2NamespaceStore = {
defaultState: StateEvent | null;
// Time series data for live values
spoolRpm: TimeSeries;
pullerSpeed: TimeSeries;
traversePosition: TimeSeries;
pullerSpeed: TimeSeries;
spoolRpm: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
targetPullerSpeed: TimeSeries;
tensionArmAngle: TimeSeries;
spoolLengthTaskProgress: TimeSeries;
};
//Store Factory and Message Handler -> no param, so default values
@@ -242,10 +251,9 @@ export const createWinder2NamespaceStore =
// Time series data for live values
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
spoolLengthTaskProgress: spoolProgress,
};
});
/**
@@ -273,28 +281,21 @@ export function winder2MessageHandler(
};
try {
if (eventName === "StateEvent") {
if (eventName === "State")
{
console.log(event);
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
const nextTargetPullerSpeed = stateEvent.data.puller_state.target_speed;
updateStore((state) => ({
...state,
state: stateEvent,
targetPullerSpeed:
state.targetPullerSpeed.current?.value === nextTargetPullerSpeed
? state.targetPullerSpeed
: addTargetPullerSpeed(state.targetPullerSpeed, {
value: nextTargetPullerSpeed,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValuesEvent") {
} else if (eventName === "LiveValues") {
// Parse and validate the live values event
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -304,7 +305,7 @@ export function winder2MessageHandler(
puller_speed,
spool_rpm,
tension_arm_angle,
spool_progress,
spool_length_task_progress: spool_progress,
} = liveValuesEvent.data;
const timestamp = liveValuesEvent.ts;
@@ -327,8 +328,8 @@ export function winder2MessageHandler(
value: spool_progress,
timestamp,
};
newState.spoolProgress = addSpoolProgress(
state.spoolProgress,
newState.spoolLengthTaskProgress = addSpoolProgress(
state.spoolLengthTaskProgress,
timeseriesValue,
);
}
@@ -27,6 +27,7 @@ impl MachineAct for AnalogInputTestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
+1
View File
@@ -61,6 +61,7 @@ impl MachineAct for AquaPathV1 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -49,6 +49,7 @@ impl MachineAct for BufferV1 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
@@ -41,6 +41,7 @@ impl MachineAct for DigitalInputTestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -77,6 +77,7 @@ impl MachineAct for ExtruderV2 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -51,6 +51,7 @@ impl MachineAct for ExtruderV2 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -80,6 +80,7 @@ impl MachineAct for ExtruderV3 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -48,6 +48,7 @@ impl MachineAct for ExtruderV2 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+139
View File
@@ -0,0 +1,139 @@
use units::angle::revolution;
use units::f64::*;
use super::{Clamp, ClampedValue};
pub fn clamp_revolution_uom(value: Angle, min: Angle, max: Angle) -> ClampedValue<Angle>
{
let value = value.get::<revolution>();
let min = min.get::<revolution>();
let max = max.get::<revolution>();
// clamp
let clamped_value = clamp_revolution(value, min, max);
// convert back to uom
ClampedValue {
value: Angle::new::<revolution>(clamped_value.value),
clamp: clamped_value.clamp
}
}
pub fn clamp_revolution(value: f64, min: f64, max: f64) -> ClampedValue<f64>
{
// normalize value from 0..1
let value = wrap_revolution(value);
let min = wrap_revolution(min);
let max = wrap_revolution(max);
// check if in acceptable range
if revolution_in_range(value, min, max) {
return ClampedValue::new(value, Clamp::None);
}
// calculates the distance between min and max
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(min, max);
// check if in min clamping range
if revolution_in_range(value, clamp_to_min_min, clamp_to_min_max) {
return ClampedValue::new(min, Clamp::Min);
}
// check if in max clamping range
if revolution_in_range(value, clamp_to_max_min, clamp_to_max_max) {
return ClampedValue::new(max, Clamp::Max);
}
// at this point our input value should be either retured (cause in spec) or clamped to min or max
// so this point should never be reached
// in case it does we just clamp to min
ClampedValue::new(min, Clamp::Min)
//TODO: consider using unreachable!() if truly unreachable
}
pub fn scale_revolution_to_range(value: f64, min: f64, max: f64) -> f64
{
// we calculate the distance between min and max
let distance = revolution_distance(min, max);
// we scale the value to the distance
(value - min) / distance
}
fn clamping_ranges(min: f64, max: f64) -> (f64, f64, f64, f64)
{
// normalize min and max
let min = wrap_revolution(min);
let max = wrap_revolution(max);
// calculates the distance between min and max (distance A in the test)
let in_spec_distance = revolution_distance(min, max);
// calculate distance B and clamping distance as per the test comment
let out_spec_distance = 1.0 - in_spec_distance;
let clamping_distance = out_spec_distance / 2.0;
let clamp_to_min_min = wrap_revolution(min - clamping_distance);
let clamp_to_min_max = min;
let clamp_to_max_min = max;
let clamp_to_max_max = wrap_revolution(max + clamping_distance);
(
clamp_to_min_min,
clamp_to_min_max,
clamp_to_max_min,
clamp_to_max_max,
)
}
fn revolution_distance(min: f64, max: f64) -> f64
{
// Normalize the values to ensure they're in the [0, 1) range
let normalized_min = wrap_revolution(min);
let normalized_max = wrap_revolution(max);
// Check if the range crosses zero
if normalized_min > normalized_max {
// For ranges that cross zero (e.g., min = 0.9, max = 0.1)
// The distance is (1 - min) + max
1.0 - normalized_min + normalized_max
} else {
// For normal ranges (e.g., min = 0.1, max = 0.3)
// The distance is simply max - min
normalized_max - normalized_min
}
}
fn wrap_revolution(value: f64) -> f64
{
debug_assert!(value.is_finite(), "value must be finite");
match value % 1.0
{
0.0 if value >= 1.0 => 1.0,
n if n < 0.0 => n + 1.0,
n => n,
}
}
fn revolution_in_range(value: f64, min: f64, max: f64) -> bool
{
debug_assert!(value.is_finite(), "value must be finite");
debug_assert!(min.is_finite(), "min must be finite");
debug_assert!(max.is_finite(), "max must be finite");
debug_assert!(value >= 0.0 && value < 1.0, "value out of [0,1)");
debug_assert!(min >= 0.0 && min < 1.0, "min out of [0,1)");
debug_assert!(max >= 0.0 && max < 1.0, "max out of [0,1)");
let wraps = min > max;
if wraps {
// Range wraps across boundary (e.g., 0.9 → 0.1)
min <= value || value <= max
} else {
// Standard interval
min <= value && value <= max
}
}
+29
View File
@@ -0,0 +1,29 @@
// keep for backwards reference
mod clamp_revolution_old;
mod clamp_revolution;
pub use clamp_revolution::clamp_revolution;
pub use clamp_revolution::clamp_revolution_uom;
pub use clamp_revolution::scale_revolution_to_range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Clamp
{
None,
Min,
Max,
}
pub struct ClampedValue<T>
{
pub value: T,
pub clamp: Clamp,
}
impl<T> ClampedValue<T>
{
pub fn new(value: T, clamp: Clamp) -> Self
{
Self { value, clamp }
}
}
+1
View File
@@ -53,6 +53,7 @@ impl MachineAct for IP20TestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+34 -6
View File
@@ -1,4 +1,5 @@
use super::LaserMachine;
use crate::MachinesLiveValues;
use crate::MachineAct;
use crate::MachineMessage;
use crate::MachineValues;
@@ -34,6 +35,26 @@ impl MachineAct for LaserMachine {
self.emit_state();
}
// send live values to all connected machines every frame
if now.duration_since(self.last_machine_send) > Duration::from_millis(1) {
let values = MachinesLiveValues::Laser(self.get_live_values());
for machine in self.connected_machines.iter() {
const CHANNEL_MESSAGE_CAP: usize = 5;
// ensure we don' flood the channel. Cap it at 5
if machine.connection.len() <= CHANNEL_MESSAGE_CAP {
use MachineMessage::ReceiveLiveValues;
_ = machine
.connection
.try_send(ReceiveLiveValues(values.clone()));
}
}
// reset timer
self.last_machine_send = now;
}
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
@@ -60,12 +81,18 @@ impl MachineAct for LaserMachine {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::ConnectToMachine(machine_connection) => {
if self.connected_machines.len() >= Self::MAX_CONNECTIONS {
tracing::debug!("Not adding machine connection. Max capacity reached!");
return;
}
self.connected_machines.push(machine_connection);
}
MachineMessage::DisconnectMachine(machine_connection) => {
self.connected_machines
.retain(|machine| machine.ident != machine_connection.ident);
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
@@ -79,6 +106,7 @@ impl MachineAct for LaserMachine {
()
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+8 -1
View File
@@ -3,7 +3,7 @@ use crate::{
MACHINE_LASER_V1, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
use crate::{Machine, MachineMessage};
use crate::{Machine, MachineConnection, MachineMessage};
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use smol::{
@@ -53,6 +53,11 @@ pub struct LaserMachine {
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
did_change_state: bool,
/// last time we sent live values to connected machines
last_machine_send: Instant,
connected_machines: Vec<MachineConnection>,
}
impl Machine for LaserMachine {
@@ -97,6 +102,8 @@ impl LaserMachine {
machine: MACHINE_LASER_V1,
};
pub const MAX_CONNECTIONS: usize = 2;
pub fn get_live_values(&self) -> LiveValuesEvent {
let diameter = self.diameter.get::<millimeter>();
let x_diameter = self.x_diameter.map(|x| x.get::<millimeter>());
+2
View File
@@ -55,6 +55,8 @@ impl MachineNewTrait for LaserMachine {
higher_tolerance: Length::new::<millimeter>(0.05),
in_tolerance: true,
did_change_state: true,
last_machine_send: Instant::now(),
connected_machines: Vec::new(),
};
Ok(laser_machine)
+29 -7
View File
@@ -26,6 +26,7 @@ pub mod extruder1;
pub mod extruder2;
pub mod ip20_test_machine;
pub mod laser;
mod live_values;
pub mod machine_identification;
pub mod mock;
pub mod motor_test_machine;
@@ -38,6 +39,11 @@ pub mod wago_ai_test_machine;
pub mod wago_do_test_machine;
pub mod wago_power;
pub mod winder2;
pub mod types;
pub mod helpers;
pub use live_values::MachinesLiveValues;
// pub use live_values::
pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
@@ -75,6 +81,7 @@ pub struct CrossConnection {
pub enum AsyncThreadMessage {
NoMsg,
ConnectOneWayRequest(CrossConnection),
ConnectTwoWayRequest(CrossConnection),
DisconnectMachines(CrossConnection),
}
@@ -307,6 +314,7 @@ pub enum MachineMessage {
ConnectToMachine(MachineConnection),
DisconnectMachine(MachineConnection),
RequestValues(Sender<MachineValues>),
ReceiveLiveValues(MachinesLiveValues),
}
pub trait MachineApi {
@@ -482,6 +490,10 @@ pub trait MachineWithChannel: Send + Debug + Sync {
fn get_live_values(&self) -> Option<Self::LiveValues> {
None
}
fn on_receive_live_values(&mut self,live_values: MachinesLiveValues) { _ = live_values; }
fn on_connect_to_machine(&mut self,connection: MachineConnection) { _ = connection; }
fn on_disconnect_machine(&mut self,connection: MachineConnection) { _ = connection; }
}
impl<C> MachineApi for C
@@ -511,9 +523,15 @@ impl<C> MachineAct for C
where
C: MachineWithChannel,
{
fn act(&mut self, now: Instant) {
while let Ok(msg) = self.get_machine_channel_mut().api_receiver.try_recv() {
self.act_machine_message(msg);
fn act(&mut self, now: Instant)
{
for _ in 0..5 // Limit to 5 requests max
{
match self.get_machine_channel_mut().api_receiver.try_recv()
{
Ok(msg) => self.act_machine_message(msg),
Err(_) => break,
}
}
if let Err(e) = self.update(now) {
@@ -535,11 +553,11 @@ where
MachineMessage::HttpApiJsonRequest(value) => {
let _ = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {
todo!();
MachineMessage::ConnectToMachine(connection) => {
self.on_connect_to_machine(connection);
}
MachineMessage::DisconnectMachine(_machine_connection) => {
todo!();
MachineMessage::DisconnectMachine(connection) => {
self.on_disconnect_machine(connection);
}
MachineMessage::RequestValues(sender) => {
sender
@@ -552,6 +570,10 @@ where
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(live_values) =>
{
self.on_receive_live_values(live_values);
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
use crate::laser::api::LiveValuesEvent as LaserLiveValues;
#[derive(Debug, Clone)]
pub enum MachinesLiveValues {
Laser(LaserLiveValues),
}
+1
View File
@@ -68,6 +68,7 @@ impl MachineAct for MockMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -41,6 +41,7 @@ impl MachineAct for TestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -28,6 +28,7 @@ impl MachineAct for TestMachineStepper {
MachineMessage::ConnectToMachine(_machine_connection) => {}
MachineMessage::DisconnectMachine(_machine_connection) => {}
MachineMessage::RequestValues(_sender) => {}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
#[derive(Debug,Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum Direction
{
Forward,
Reverse,
}
impl Direction
{
pub fn from_bool(forward: bool) -> Self
{
match forward
{
true => Self::Forward,
false => Self::Reverse,
}
}
pub fn is_forward(self) -> bool
{
self == Self::Forward
}
pub fn is_reverse(self) -> bool
{
self != Self::Forward
}
}
@@ -41,6 +41,7 @@ impl MachineAct for Wago8chDigitalIOTestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1
View File
@@ -25,6 +25,7 @@ impl MachineAct for WagoAiTestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
+1
View File
@@ -41,6 +41,7 @@ impl MachineAct for WagoDOTestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
-77
View File
@@ -1,77 +0,0 @@
#[cfg(not(feature = "mock-machine"))]
use super::Winder2;
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineAct, MachineMessage, MachineValues};
#[cfg(not(feature = "mock-machine"))]
use std::time::{Duration, Instant};
#[cfg(not(feature = "mock-machine"))]
impl MachineAct for Winder2 {
fn act(&mut self, now: Instant) {
let machine_message = self.api_receiver.try_recv();
match machine_message {
Ok(machine_message) => self.act_machine_message(machine_message),
Err(_e) => (),
};
// sync the spool speed
self.sync_spool_speed(now);
// sync the puller speed
self.sync_puller_speed(now);
// sync the traverse speed
self.sync_traverse_speed();
// automatically stops or pulls after N Meters if enabled
self.stop_or_pull_spool(now);
if self.traverse_controller.did_change_state() {
self.emit_state();
}
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(machine_connection) => {
if self.connected_machines.len() >= self.max_connected_machines {
tracing::debug!(
"Refusing to add Machine Connection {:?}, since self.connected_machines would be over the limit of {:?}",
machine_connection,
self.max_connected_machines
);
return;
}
self.connected_machines.push(machine_connection);
}
MachineMessage::DisconnectMachine(_machine_connection) => {
self.connected_machines.clear();
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.build_state_event())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
}
-402
View File
@@ -1,402 +0,0 @@
#[cfg(feature = "mock-machine")]
mod winder2_imports {
pub use super::super::Winder2Mode;
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
};
pub use control_core_derive::BuildEvent;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value;
pub use smol::lock::Mutex;
pub use std::{
sync::Arc,
time::{Duration, Instant},
};
pub use tracing::instrument;
}
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use super::super::{Winder2, Winder2Mode};
pub use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
};
pub use control_core_derive::BuildEvent;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value;
pub use smol::lock::Mutex;
pub use std::{
sync::Arc,
time::{Duration, Instant},
};
pub use tracing::instrument;
}
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineApi, MachineMessage};
use crate::{MachineCrossConnectionState, machine_identification::MachineIdentificationUnique};
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum Mode {
#[default]
Standby,
Hold,
Pull,
Wind,
}
impl From<Winder2Mode> for Mode {
fn from(mode: Winder2Mode) -> Self {
match mode {
Winder2Mode::Standby => Self::Standby,
Winder2Mode::Hold => Self::Hold,
Winder2Mode::Pull => Self::Pull,
Winder2Mode::Wind => Self::Wind,
}
}
}
impl From<Mode> for Winder2Mode {
fn from(mode: Mode) -> Self {
match mode {
Mode::Standby => Self::Standby,
Mode::Hold => Self::Hold,
Mode::Pull => Self::Pull,
Mode::Wind => Self::Wind,
}
}
}
#[derive(Deserialize, Serialize)]
pub enum Mutation {
// Traverse
/// Position in mm from home point
SetTraverseLimitOuter(f64),
/// Position in mm from home point
SetTraverseLimitInner(f64),
/// Step size in mm for traverse movement
SetTraverseStepSize(f64),
/// Padding in mm for traverse movement limits
SetTraversePadding(f64),
GotoTraverseLimitOuter,
GotoTraverseLimitInner,
/// Find home point
GotoTraverseHome,
EnableTraverseLaserpointer(bool),
// Puller
/// on = speed, off = stop
SetPullerRegulationMode(PullerRegulationMode),
SetPullerTargetSpeed(f64),
SetPullerTargetDiameter(f64),
SetPullerForward(bool),
SetPullerGearRatio(GearRatio),
// Spool Speed Controller
SetSpoolRegulationMode(super::spool_speed_controller::SpoolSpeedControllerType),
SetSpoolMinMaxMinSpeed(f64),
SetSpoolMinMaxMaxSpeed(f64),
SetSpoolForward(bool),
// Adaptive Spool Speed Controller Parameters
SetSpoolAdaptiveTensionTarget(f64),
SetSpoolAdaptiveRadiusLearningRate(f64),
SetSpoolAdaptiveMaxSpeedMultiplier(f64),
SetSpoolAdaptiveAccelerationFactor(f64),
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(f64),
// Spool Auto Stop/Pull
SetSpoolAutomaticRequiredMeters(f64),
SetSpoolAutomaticAction(SpoolAutomaticActionMode),
ResetSpoolProgress,
// Tension Arm
ZeroTensionArmAngle,
// Mode
SetMode(Mode),
// Connected Machine
SetConnectedMachine(MachineIdentificationUnique),
// Disconnect Machine
DisconnectMachine(MachineIdentificationUnique),
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {
/// traverse position in mm
pub traverse_position: Option<f64>,
/// puller speed in m/min
pub puller_speed: f64,
/// spool rpm
pub spool_rpm: f64,
/// tension arm angle in degrees
pub tension_arm_angle: f64,
// spool progress in meters (pulled distance of filament)
pub spool_progress: f64,
}
impl LiveValuesEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, BuildEvent)]
pub struct StateEvent {
pub is_default_state: bool,
/// traverse state
pub traverse_state: TraverseState,
/// puller state
pub puller_state: PullerState,
/// spool automatic action state and progress
pub spool_automatic_action_state: SpoolAutomaticActionState,
/// mode state
pub mode_state: ModeState,
/// tension arm state
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// Is a Machine Connected?
pub connected_machine_state: MachineCrossConnectionState,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct TraverseState {
/// min position in mm
pub limit_inner: f64,
/// max position in mm
pub limit_outer: f64,
/// position in mm
pub position_in: f64,
/// position out in mm
pub position_out: f64,
/// is going to position in
pub is_going_in: bool,
/// is going to position out
pub is_going_out: bool,
/// if is homed
pub is_homed: bool,
/// if is homing
pub is_going_home: bool,
/// if is traversing
pub is_traversing: bool,
/// laserpointer is on
pub laserpointer: bool,
/// step size in mm
pub step_size: f64,
/// padding in mm
pub padding: f64,
/// can go in (to inner limit)
pub can_go_in: bool,
/// can go out (to outer limit)
pub can_go_out: bool,
/// can home
pub can_go_home: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct PullerState {
/// regulation type
pub regulation: PullerRegulationMode,
/// target speed in m/min
pub target_speed: f64,
/// target diameter in mm
pub target_diameter: f64,
/// forward rotation direction
pub forward: bool,
/// gear ratio for winding speed
pub gear_ratio: GearRatio,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub enum SpoolAutomaticActionMode {
#[default]
NoAction,
Pull,
Hold,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolAutomaticActionState {
pub spool_required_meters: f64,
pub spool_automatic_action_mode: SpoolAutomaticActionMode,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct ModeState {
/// mode
pub mode: Mode,
/// can wind
pub can_wind: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct TensionArmState {
/// is zeroed
pub zeroed: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolSpeedControllerState {
/// regulation mode
pub regulation_mode: super::spool_speed_controller::SpoolSpeedControllerType,
/// min speed in rpm for minmax mode
pub minmax_min_speed: f64,
/// max speed in rpm for minmax mode
pub minmax_max_speed: f64,
/// tension target for adaptive mode (0.0-1.0)
pub adaptive_tension_target: f64,
/// radius learning rate for adaptive mode
pub adaptive_radius_learning_rate: f64,
/// max speed multiplier for adaptive mode
pub adaptive_max_speed_multiplier: f64,
/// acceleration factor for adaptive mode
pub adaptive_acceleration_factor: f64,
/// deacceleration urgency multiplier for adaptive mode
pub adaptive_deacceleration_urgency_multiplier: f64,
/// forward rotation direction
pub forward: bool,
}
pub enum Winder2Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
#[derive(Debug)]
pub struct Winder2Namespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<Winder2Events> for Winder2Namespace {
#[instrument(skip_all)]
fn emit(&mut self, events: Winder2Events) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
impl CacheableEvents<Self> for Winder2Events {
fn event_value(&self) -> GenericEvent {
match self {
Self::LiveValues(event) => event.into(),
Self::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for Winder2 {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
use crate::Machine;
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::EnableTraverseLaserpointer(enable) => self.set_laser(enable),
Mutation::SetMode(mode) => self.set_mode(&mode.into()),
Mutation::SetTraverseLimitOuter(limit) => self.traverse_set_limit_outer(limit),
Mutation::SetTraverseLimitInner(limit) => self.traverse_set_limit_inner(limit),
Mutation::SetTraverseStepSize(size) => self.traverse_set_step_size(size),
Mutation::SetTraversePadding(padding) => self.traverse_set_padding(padding),
Mutation::GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
Mutation::GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
Mutation::GotoTraverseHome => self.traverse_goto_home(),
Mutation::SetPullerRegulationMode(regulation) => self.puller_set_regulation(regulation),
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
Mutation::SetPullerTargetDiameter(_) => todo!(),
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
self.spool_set_adaptive_tension_target(value)
}
Mutation::SetSpoolAdaptiveRadiusLearningRate(value) => {
self.spool_set_adaptive_radius_learning_rate(value)
}
Mutation::SetSpoolAdaptiveMaxSpeedMultiplier(value) => {
self.spool_set_adaptive_max_speed_multiplier(value)
}
Mutation::SetSpoolAdaptiveAccelerationFactor(value) => {
self.spool_set_adaptive_acceleration_factor(value)
}
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
}
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
self.set_spool_automatic_required_meters(meters)
}
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
Mutation::SetConnectedMachine(machine_identification_unique) => {
let main_sender = match &self.main_sender {
Some(sender) => sender,
None => {
return Err(anyhow::anyhow!(
"Machine cannot connect to others! {:?}",
self.get_machine_identification_unique()
));
}
};
main_sender.try_send(crate::AsyncThreadMessage::ConnectOneWayRequest(
crate::CrossConnection {
src: self.get_machine_identification_unique(),
dest: machine_identification_unique,
},
))?;
self.emit_state();
}
Mutation::DisconnectMachine(_machine_identification_unique) => {
self.connected_machines.clear();
/*let main_sender = match &self.main_sender {
Some(sender) => sender,
None => return Err(anyhow::anyhow!("[DisconnectMachine] Machine cannot connect to others! {:?}", self.machine_identification_unique)),
};*/
//main_sender.try_send(crate::AsyncThreadMessage::ConnectOneWayRequest(crate::CrossConnection { src: self.get_machine_identification_unique(), dest: machine_identification_unique }))?;
self.emit_state();
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
+65
View File
@@ -0,0 +1,65 @@
use std::time::Duration;
use control_core::socketio::{event::{BuildEvent, GenericEvent}, namespace::{CacheFn, CacheableEvents, cache_duration}};
use control_core_derive::BuildEvent;
use serde::Serialize;
use units::{angle::degree, angular_velocity::revolution_per_minute, length::{meter, millimeter}, velocity::meter_per_minute};
use crate::winder2::Winder2;
mod state;
pub use state::State;
mod mutation;
// Live values
#[derive(Serialize, Debug, Clone, Default, BuildEvent)]
pub struct LiveValues
{
/// traverse position in mm
pub traverse_position: Option<f64>,
/// puller speed in m/min
pub puller_speed: f64,
/// spool rpm
pub spool_rpm: f64,
/// tension arm angle in degrees
pub tension_arm_angle: f64,
// spool progress in meters (pulled distance of filament)
pub spool_length_task_progress: f64,
}
impl CacheableEvents<Self> for LiveValues
{
fn event_value(&self) -> GenericEvent
{
self.build().into()
}
fn event_cache_fn(&self) -> CacheFn {
cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1))
}
}
impl Winder2
{
pub fn create_live_values(&self) -> LiveValues
{
let tension_arm_angle = self.tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
// This is done to reduce flicker in the graphs around the zero point
let tension_arm_angle = match tension_arm_angle >= 270.0
{
true => tension_arm_angle - 360.0,
false => tension_arm_angle,
};
LiveValues {
traverse_position: self.traverse.current_position().map(|x| x.get::<millimeter>()),
puller_speed: self.puller.output_speed().get::<meter_per_minute>(),
spool_rpm: self.spool.speed().get::<revolution_per_minute>(),
spool_length_task_progress: self.spool_length_task.current_length().get::<meter>(),
tension_arm_angle,
}
}
}
+477
View File
@@ -0,0 +1,477 @@
use std::time::Instant;
use serde::{Deserialize, Serialize};
use units::{
AngularVelocity,
Length,
Velocity,
angular_velocity::revolution_per_minute,
length::{meter, millimeter},
velocity::meter_per_minute
};
use crate::{
AsyncThreadMessage, CrossConnection, Machine, machine_identification::MachineIdentificationUnique, types::Direction, winder2::{Winder2, devices::{
OperationState,
PullerGearRatio,
PullerSpeedControlMode,
SpoolSpeedControlMode
}, types::{Mode, SpoolLengthTaskCompletedAction}}
};
#[derive(Deserialize, Serialize)]
pub enum Mutation
{
// Machine
SetMode(Mode),
// Spool
SetSpoolDirection(Direction),
SetSpoolSpeedControlMode(SpoolSpeedControlMode),
// Spool Speed MinMax Strategy
SetSpoolMinMaxMinSpeed(f64),
SetSpoolMinMaxMaxSpeed(f64),
// Spool Speed Adaptive Strategy
SetSpoolAdaptiveTensionTarget(f64),
SetSpoolAdaptiveRadiusLearningRate(f64),
SetSpoolAdaptiveMaxSpeedMultiplier(f64),
SetSpoolAdaptiveAccelerationFactor(f64),
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(f64),
// Puller
SetPullerDirection(Direction),
SetPullerGearRatio(PullerGearRatio),
SetPullerSpeedControlMode(PullerSpeedControlMode),
// Puller Speed Fixed Strategy
SetPullerFixedTargetSpeed(f64),
// Puller Speed Strategy
SetPullerAdaptiveBaseSpeed(f64), // in m/min
SetPullerAdaptiveDeviationMax(f64), // in m/min
SetPullerAdaptiveReferenceMachine(Option<MachineIdentificationUnique>),
// Traverse
/// Position in mm from home point
SetTraverseLimitInner(f64),
/// Position in mm from home point
SetTraverseLimitOuter(f64),
/// Step size in mm for traverse movement
SetTraverseStepSize(f64),
/// Padding in mm for traverse movement limits
SetTraversePadding(f64),
GotoTraverseLimitInner,
GotoTraverseLimitOuter,
/// Find home point
GotoTraverseHome,
SetTraverseLaserpointerEnabled(bool),
// Tension Arm
CalibrateTensionArmAngle,
// Spool Length Task
SetSpoolLengthTaskTargetLength(f64),
ResetSetSpoolLengthTaskProgress,
SetOnSpoolLengthTaskCompletedAction(SpoolLengthTaskCompletedAction),
}
impl Winder2
{
pub fn handle_mutation(&mut self, value: serde_json::Value) -> Result<(), anyhow::Error>
{
use Mutation::*;
let mutation: Mutation = serde_json::from_value(value)?;
match mutation
{
// machine
SetMode(v) => self.set_mode(v),
// spool
SetSpoolDirection(v) => self.spool_set_direction(v),
SetSpoolSpeedControlMode(v) => self.spool_set_speed_control_mode(v),
SetSpoolMinMaxMinSpeed(v) => self.spool_set_minmax_min_speed(v),
SetSpoolMinMaxMaxSpeed(v) => self.spool_set_minmax_max_speed(v),
SetSpoolAdaptiveTensionTarget(v) => self.spool_set_adaptive_tension_target(v),
SetSpoolAdaptiveRadiusLearningRate(v) => self.spool_set_adaptive_radius_learning_rate(v),
SetSpoolAdaptiveMaxSpeedMultiplier(v) => self.spool_set_adaptive_max_speed_multiplier(v),
SetSpoolAdaptiveAccelerationFactor(v) => self.spool_set_adaptive_acceleration_factor(v),
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(v) => self.spool_set_adaptive_deacceleration_urgency_multiplier(v),
//traverse
SetTraverseLimitInner(v) => self.traverse_set_limit_inner(v),
SetTraverseLimitOuter(v) => self.traverse_set_limit_outer(v),
SetTraverseStepSize(v) => self.traverse_set_step_size(v),
SetTraversePadding(v) => self.traverse_set_padding(v),
GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
GotoTraverseHome => self.traverse_goto_home(),
SetTraverseLaserpointerEnabled(v) => self.traverse_set_laser_pointer_enabled(v),
// puller
SetPullerDirection(v) => self.puller_set_direction(v),
SetPullerGearRatio(v) => self.puller_set_gear_ratio(v),
SetPullerSpeedControlMode(v) => self.puller_set_speed_control_mode(v),
SetPullerFixedTargetSpeed(v) => self.puller_set_fixed_target_speed(v),
SetPullerAdaptiveBaseSpeed(v) => self.puller_set_adaptive_base_speed(v),
SetPullerAdaptiveDeviationMax(v) => self.puller_set_adaptive_deviation_max(v),
SetPullerAdaptiveReferenceMachine(v) => self.puller_set_adaptive_reference_machine(v)?,
// tension arm
CalibrateTensionArmAngle => self.tension_arm_calibrate(),
// spool length task
SetSpoolLengthTaskTargetLength(v) =>
self.spool_length_task_set_target_length(v),
ResetSetSpoolLengthTaskProgress =>
self.spool_length_task_reset(Instant::now()),
SetOnSpoolLengthTaskCompletedAction(v) =>
self.set_on_spool_length_task_complete(v),
}
Ok(())
}
}
// Machine
impl Winder2
{
pub fn set_mode(&mut self, mode: Mode)
{
let should_update = mode != Mode::Wind || self.can_wind();
if should_update
{
self.mode = mode;
match self.mode
{
Mode::Standby =>
{
let state = OperationState::Disabled;
self.spool.set_operation_state(state);
self.puller.set_operation_state(state);
self.traverse.set_operation_state(state);
},
Mode::Hold =>
{
let state = OperationState::Holding;
self.spool.set_operation_state(state);
self.puller.set_operation_state(state);
self.traverse.set_operation_state(state);
},
Mode::Pull =>
{
use OperationState::*;
self.spool.set_operation_state(Holding);
self.puller.set_operation_state(Running);
self.traverse.set_operation_state(Holding);
},
Mode::Wind =>
{
let state = OperationState::Running;
self.spool.set_operation_state(state);
self.puller.set_operation_state(state);
self.traverse.set_operation_state(state);
},
}
}
self.emit_state();
}
}
// Spool
impl Winder2
{
pub fn spool_set_direction(&mut self, value: Direction)
{
self.spool.set_direction(value);
self.emit_state();
}
pub fn spool_set_speed_control_mode(&mut self, value: SpoolSpeedControlMode)
{
self.spool.set_speed_control_mode(value);
self.emit_state();
}
/// Set minimum speed for minmax mode in RPM
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64)
{
let min_speed = AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
if let Err(e) = self.spool.speed_controllers.minmax.set_min_speed(min_speed)
{
tracing::error!("Failed to set spool min speed: {:?}", e);
}
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64)
{
let max_speed = AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
if let Err(e) = self.spool.speed_controllers.minmax.set_max_speed(max_speed)
{
tracing::error!("Failed to set spool max speed: {:?}", e);
}
self.emit_state();
}
/// Set tension target for adaptive mode (0.0-1.0)
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64)
{
self.spool.speed_controllers.adaptive.set_tension_target(tension_target);
self.emit_state();
}
/// Set radius learning rate for adaptive mode
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.spool.speed_controllers.adaptive
.set_radius_learning_rate(radius_learning_rate);
self.emit_state();
}
/// Set max speed multiplier for adaptive mode
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.spool.speed_controllers.adaptive
.set_max_speed_multiplier(max_speed_multiplier);
self.emit_state();
}
/// Set acceleration factor for adaptive mode
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.spool.speed_controllers.adaptive
.set_acceleration_factor(acceleration_factor);
self.emit_state();
}
/// Set deacceleration urgency multiplier for adaptive mode
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.spool.speed_controllers.adaptive
.set_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
self.emit_state();
}
}
// Puller
impl Winder2
{
pub fn puller_set_speed_control_mode(&mut self, mode: PullerSpeedControlMode)
{
self.puller.set_speed_control_mode(mode);
self.emit_state();
}
pub fn puller_set_direction(&mut self, direction: Direction)
{
self.puller.set_direction(direction);
self.emit_state();
}
pub fn puller_set_gear_ratio(&mut self, gear_ratio: PullerGearRatio)
{
self.puller.set_gear_ratio(gear_ratio);
// safety measure when changing gear ratio
self.set_mode(Mode::Standby);
self.emit_state();
}
/// target_speed in m/min
pub fn puller_set_fixed_target_speed(&mut self, speed: f64)
{
// Convert m/min to velocity
let speed = Velocity::new::<meter_per_minute>(speed);
self.puller.speed_controller_strategies_mut().fixed.set_target_speed(speed);
self.emit_state();
}
pub fn puller_set_adaptive_base_speed(&mut self, speed: f64)
{
let speed = Velocity::new::<meter_per_minute>(speed);
self.puller.speed_controller_strategies_mut().adaptive.set_base_speed(speed);
self.emit_state();
}
pub fn puller_set_adaptive_deviation_max(&mut self, deviation_max: f64)
{
let speed = Velocity::new::<meter_per_minute>(deviation_max);
self.puller.speed_controller_strategies_mut().adaptive.set_deviation_max(speed);
self.emit_state();
}
pub fn puller_set_adaptive_reference_machine(
&mut self,
machine_uid: Option<MachineIdentificationUnique>
) -> Result<(), anyhow::Error>
{
match machine_uid
{
Some(machine_uid) =>
{
if let Some(connection) = &self.puller_speed_reference_machine
{
if connection.ident == machine_uid
{ return Ok(()); }
}
let main_sender = match &self.channel.main_sender
{
Some(v) => v,
None =>
{
return Err(anyhow::anyhow!(
"{:?} Failed to connect to {:?}",
self.get_machine_identification_unique(),
machine_uid,
));
}
};
main_sender.try_send(AsyncThreadMessage::ConnectTwoWayRequest(
CrossConnection {
src: self.get_machine_identification_unique(),
dest: machine_uid,
},
))?;
self.emit_state();
},
None =>
{
match self.puller_speed_reference_machine.take()
{
Some(connection) =>
{
_ = connection;
//TODO: Tell other machine to disconnect
let main_sender = match &self.channel.main_sender
{
Some(v) => v,
None =>
{
return Err(anyhow::anyhow!(
"{:?} Failed to connect to {:?}",
self.get_machine_identification_unique(),
machine_uid,
));
}
};
main_sender.try_send(AsyncThreadMessage::DisconnectMachines(
CrossConnection {
src: self.get_machine_identification_unique(),
dest: connection.ident,
},
))?;
},
None => return Ok(()), // nothing to do
}
},
}
self.emit_state();
Ok(())
}
}
// Traverse
impl Winder2
{
pub fn traverse_set_laser_pointer_enabled(&mut self, value: bool)
{
self.traverse.set_laser_pointer_enabled(value);
self.emit_state();
}
pub fn traverse_set_limit_inner(&mut self, limit: f64)
{
let limit_inner = Length::new::<millimeter>(limit);
_ = self.traverse.try_set_limit_inner(limit_inner);
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64)
{
let limit_outer = Length::new::<millimeter>(limit);
_ = self.traverse.try_set_limit_outer(limit_outer);
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64)
{
let step_size = Length::new::<millimeter>(step_size);
self.traverse.set_step_size(step_size);
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64)
{
let padding = Length::new::<millimeter>(padding);
self.traverse.set_padding(padding);
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self)
{
_ = self.traverse.try_goto_limit_inner();
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self)
{
_ = self.traverse.try_goto_limit_outer();
self.emit_state();
}
pub fn traverse_goto_home(&mut self)
{
_ = self.traverse.try_goto_home();
self.emit_state();
}
}
// Tension Arm
impl Winder2
{
pub fn tension_arm_calibrate(&mut self)
{
self.tension_arm.calibrate();
self.emit_live_values(); // For angle update
self.emit_state();
}
}
// Spool Length Task
impl Winder2
{
pub fn spool_length_task_set_target_length(&mut self, meters: f64)
{
let target_length = Length::new::<meter>(meters);
self.spool_length_task.set_target_length(target_length);
self.emit_state();
}
pub fn spool_length_task_reset(&mut self, now: Instant)
{
self.spool_length_task.reset(now);
self.emit_state();
}
pub fn set_on_spool_length_task_complete(&mut self, action: SpoolLengthTaskCompletedAction)
{
self.on_spool_length_task_complete = action;
self.emit_state();
}
}
+235
View File
@@ -0,0 +1,235 @@
use control_core::socketio::{event::{BuildEvent, GenericEvent}, namespace::{CacheFn, CacheableEvents, cache_first_and_last_event}};
use control_core_derive::BuildEvent;
use serde::Serialize;
use units::{angular_velocity::revolution_per_minute, length::{meter, millimeter}, velocity::meter_per_minute};
use crate::{
MachineCrossConnectionState, types::Direction, winder2::{
Winder2,
devices::{
PullerGearRatio, PullerSpeedControlMode, SpoolSpeedControlMode
}, types::{Mode, SpoolLengthTaskCompletedAction}
}
};
#[derive(Serialize, Debug, Clone, BuildEvent)]
pub struct State
{
// common
pub is_default_state: bool,
// machine
pub mode: Mode,
pub can_wind: bool,
// devices
pub spool_state: SpoolState,
pub puller_state: PullerState,
pub traverse_state: TraverseState,
pub tension_arm_state: TensionArmState,
// tasks
pub spool_length_task_state: SpoolLengthTaskState,
}
impl CacheableEvents<Self> for State
{
fn event_value(&self) -> GenericEvent {
self.build().into()
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
#[derive(Serialize, Debug, Clone)]
pub struct SpoolState
{
pub direction: Direction,
pub speed_control_mode: SpoolSpeedControlMode,
/// min max mode
pub minmax_min_speed: f64, // in rpm
pub minmax_max_speed: f64, // in rpm
// adaptive mode
pub adaptive_tension_target: f64,
pub adaptive_radius_learning_rate: f64,
pub adaptive_max_speed_multiplier: f64,
pub adaptive_acceleration_factor: f64,
pub adaptive_deacceleration_urgency_multiplier: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct PullerState
{
pub direction: Direction,
pub gear_ratio: PullerGearRatio,
pub speed_control_mode: PullerSpeedControlMode,
// fixed speed strategy
pub fixed_target_speed: f64, // in m/min
// adaptive speed strategy
pub adaptive_base_speed: f64, // in m/min
pub adaptive_deviation_max: f64, // in m/min
pub adaptive_reference_machine: MachineCrossConnectionState,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct TraverseState {
// config
pub limit_inner: f64, /// in mm
pub limit_outer: f64, /// in mm
pub step_size: f64, /// in mm
pub padding: f64, /// in mm
// states
pub is_going_in: bool,
pub is_going_out: bool,
pub is_homed: bool,
pub is_going_home: bool,
pub is_traversing: bool,
// state transitions
pub can_go_in: bool,
pub can_go_out: bool,
pub can_go_home: bool,
// lazeeeeeeeer
pub laserpointer_enabled: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct TensionArmState
{
pub is_calibrated: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct SpoolLengthTaskState
{
pub target_length: f64,
pub on_completed_action: SpoolLengthTaskCompletedAction,
}
impl Winder2
{
pub fn create_state(&self) -> State
{
let is_default_state = !self.emitted_default_state;
State {
is_default_state,
mode: self.mode,
can_wind: self.can_wind(),
traverse_state: self.create_traverse_state(),
puller_state: self.create_puller_state(),
spool_length_task_state: self.create_spool_length_task_state(),
tension_arm_state: self.create_tension_arm_state(),
spool_state: self.create_spool_state(),
}
}
fn create_spool_state(&self) -> SpoolState
{
use revolution_per_minute as rpm;
let speed_controllers = &self.spool.speed_controllers;
let minmax = &speed_controllers.minmax;
let adaptive = &speed_controllers.adaptive;
let adaptive_deacceleration_urgency_multiplier =
adaptive.deacceleration_urgency_multiplier();
SpoolState {
direction: self.spool.direction(),
speed_control_mode: self.spool.speed_control_mode(),
// min max speed controller
minmax_min_speed: minmax.min_speed().get::<rpm>(),
minmax_max_speed: minmax.max_speed().get::<rpm>(),
// adaptive speed controller
adaptive_tension_target: adaptive.tension_target(),
adaptive_radius_learning_rate: adaptive.radius_learning_rate(),
adaptive_max_speed_multiplier: adaptive.max_speed_multiplier(),
adaptive_acceleration_factor: adaptive.acceleration_factor(),
adaptive_deacceleration_urgency_multiplier,
}
}
fn create_puller_state(&self) -> PullerState
{
let puller = &self.puller;
let strategies = puller.speed_controller_strategies();
let fixed_target_speed =
strategies.fixed.target_speed().get::<meter_per_minute>();
let adaptive_base_speed =
strategies.adaptive.base_speed().get::<meter_per_minute>();
let adaptive_deviation_max =
strategies.adaptive.deviation_max().get::<meter_per_minute>();
let adaptive_reference_machine = match &self.puller_speed_reference_machine
{
Some(connection) => MachineCrossConnectionState {
machine_identification_unique: Some(connection.ident.clone()),
is_available: true
},
None => MachineCrossConnectionState {
machine_identification_unique: None,
is_available: false
},
};
PullerState {
direction: puller.direction(),
gear_ratio: puller.gear_ratio(),
speed_control_mode: puller.speed_control_mode(),
fixed_target_speed,
adaptive_base_speed,
adaptive_deviation_max,
adaptive_reference_machine,
}
}
fn create_traverse_state(&self) -> TraverseState
{
let traverse = &self.traverse;
TraverseState {
limit_inner: traverse.limit_inner().get::<millimeter>(),
limit_outer: traverse.limit_outer().get::<millimeter>(),
is_going_in: traverse.is_going_in(),
is_going_out: traverse.is_going_out(),
is_homed: traverse.is_homed(),
is_going_home: traverse.is_going_home(),
is_traversing: traverse.is_traversing(),
laserpointer_enabled: traverse.laser_pointer_enabled(),
step_size: traverse.step_size().get::<millimeter>(),
padding: traverse.padding().get::<millimeter>(),
can_go_in: traverse.can_goto_limit_inner(),
can_go_out: traverse.can_goto_limit_outer(),
can_go_home: traverse.can_go_home()
}
}
fn create_tension_arm_state(&self) -> TensionArmState
{
TensionArmState {
is_calibrated: self.tension_arm.is_calibrated()
}
}
fn create_spool_length_task_state(&self) -> SpoolLengthTaskState
{
SpoolLengthTaskState {
target_length: self.spool_length_task.target_length().get::<meter>(),
on_completed_action: self.on_spool_length_task_complete,
}
}
}
+22
View File
@@ -0,0 +1,22 @@
mod spool;
pub use spool::Spool;
pub use spool::SpeedControlMode as SpoolSpeedControlMode;
mod puller;
pub use puller::Puller;
pub use puller::GearRatio as PullerGearRatio;
pub use puller::SpeedControlMode as PullerSpeedControlMode;
mod tension_arm;
pub use tension_arm::TensionArm;
mod traverse;
pub use traverse::Traverse;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationState
{
Disabled,
Holding,
Running,
}
+234
View File
@@ -0,0 +1,234 @@
use std::time::Instant;
use serde::{Deserialize, Serialize};
use units::ConstZero;
use units::{
acceleration::meter_per_minute_per_second,
f64::*,
jerk::meter_per_minute_per_second_squared,
length::centimeter,
velocity::meter_per_minute,
};
use meter_per_minute as velocity_unit;
use meter_per_minute_per_second as acceleration_unit;
use meter_per_minute_per_second_squared as jerk_unit;
use control_core::{ converters::linear_step_converter::LinearStepConverter };
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use crate::types::Direction;
use super::OperationState;
mod speed_controller;
pub use speed_controller::SpeedController;
pub use speed_controller::{
Mode as SpeedControlMode,
Strategies as SpeedControllerStrategies
};
/// Represents the puller motor
#[derive(Debug)]
pub struct Puller
{
// hardware
motor: StepperVelocityEL70x1,
// config
operation_state: OperationState,
direction: Direction,
gear_ratio: GearRatio,
// misc
speed_controller: SpeedController,
step_converter: LinearStepConverter,
}
// constants
impl Puller
{
// fixed
const STEPS_PER_REVOLUTION: i16 = 200; // in steps
const WHEEL_DIAMETER: f64 = 8.0; // in cm
const MOTOR_SPEED_MAX: f64 = 50.0; // in meters per minute
const MOTOR_ACCELERATION_MAX: f64 = 5.0; // in meters per minute per second
const MOTOR_JERK_MAX: f64 = 10.0; // in meters per minute per second squared
}
// public interface
impl Puller
{
pub fn new(motor: StepperVelocityEL70x1) -> Self
{
let speed_controller = {
let speed_max = Velocity::new::<velocity_unit>(Self::MOTOR_SPEED_MAX);
let acc_max = Acceleration::new::<acceleration_unit>(Self::MOTOR_ACCELERATION_MAX);
let jerk_max = Jerk::new::<jerk_unit>(Self::MOTOR_JERK_MAX);
SpeedController::new(speed_max, jerk_max, acc_max)
};
let step_converter = {
let diameter = Length::new::<centimeter>(Self::WHEEL_DIAMETER);
LinearStepConverter::from_diameter(Self::STEPS_PER_REVOLUTION, diameter)
};
Self {
// config
operation_state: OperationState::Disabled,
direction: Direction::Forward,
gear_ratio: GearRatio::OneToOne,
// misc
motor,
speed_controller,
step_converter,
}
}
pub fn update(&mut self, t: Instant)
{
// update speed
self.speed_controller.update(t);
// retrieve speed
let speed = self.speed_controller.speed();
// only disable motor once it reaches a really low speed
// to prevent an abrupt stop
if self.operation_state == OperationState::Disabled
&& speed.get::<meter_per_minute>() <= 0.1
{
self.motor.set_enabled(false);
}
// convert to steps/sec
let steps_per_second = self.step_converter.velocity_to_steps(speed);
// write to hardware
_ = self.motor.set_speed(steps_per_second);
}
}
// getter + setter
impl Puller
{
#[allow(dead_code)]
pub fn operation_state(&self) -> OperationState
{
self.operation_state
}
pub fn set_operation_state(&mut self, operation_state: OperationState)
{
use OperationState::*;
// No change, nothing to do
if self.operation_state == operation_state { return; }
// Leaving disabled state, enable motor
if self.operation_state == Disabled {
self.motor.set_enabled(true);
}
self.speed_controller.set_enabled(operation_state == Running);
self.operation_state = operation_state;
}
pub fn direction(&self) -> Direction
{
self.direction
}
pub fn set_direction(&mut self, direction: Direction)
{
self.direction = direction;
self.update_multiplier();
}
pub fn gear_ratio(&self) -> GearRatio
{
self.gear_ratio
}
pub fn set_gear_ratio(&mut self, gear_ratio: GearRatio)
{
self.gear_ratio = gear_ratio;
self.update_multiplier();
// reset configured speeds when gear ratio changes
// for added safety, since changing gear ratio should
// require a reconfiguration of the machine
let strategies = self.speed_controller.strategies_mut();
strategies.fixed.set_target_speed(Velocity::ZERO);
strategies.adaptive.set_base_speed(Velocity::ZERO);
strategies.adaptive.set_deviation_max(Velocity::ZERO);
}
pub fn output_speed(&self) -> Velocity
{
self.speed_controller.speed() / self.gear_ratio.multiplier()
}
pub fn speed_control_mode(&self) -> SpeedControlMode
{
self.speed_controller.mode()
}
pub fn set_speed_control_mode(&mut self, mode: SpeedControlMode)
{
self.speed_controller.set_mode(mode);
}
pub fn speed_controller_strategies(&self) -> &SpeedControllerStrategies
{
self.speed_controller.strategies()
}
pub fn speed_controller_strategies_mut(&mut self) -> &mut SpeedControllerStrategies
{
self.speed_controller.strategies_mut()
}
}
// utils
impl Puller
{
fn update_multiplier(&mut self)
{
let multiplier = self.gear_ratio.multiplier();
let multiplier = match self.direction
{
Direction::Forward => multiplier,
Direction::Reverse => -multiplier,
};
self.speed_controller.set_multiplier(multiplier);
}
}
// other types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GearRatio
{
OneToOne,
FiveToOne,
TenToOne,
}
impl GearRatio
{
/// Get the speed multiplier for this gear ratio
pub fn multiplier(&self) -> f64
{
match self
{
GearRatio::OneToOne => 1.0,
GearRatio::FiveToOne => 5.0,
GearRatio::TenToOne => 10.0,
}
}
}
@@ -0,0 +1,175 @@
use std::time::Instant;
use control_core::controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController;
use serde::{Deserialize, Serialize};
use units::{Acceleration, ConstZero, Jerk, Velocity};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Mode
{
Fixed,
Adaptive,
}
#[derive(Debug)]
pub struct SpeedController
{
// config
mode: Mode,
enabled: bool,
multiplier: f64,
// state
speed: Velocity,
strategies: Strategies,
acceleration_controller: LinearJerkSpeedController,
}
impl SpeedController
{
pub fn new(speed_max: Velocity, jerk_max: Jerk, acceleration_max: Acceleration) -> Self
{
let fixed = FixedSpeedStrategy {
target_speed: Velocity::ZERO
};
let adaptive = AdaptiveSpeedStrategy {
base_speed: Velocity::ZERO,
deviation_max: Velocity::ZERO,
modulation: 0.0,
};
let acceleration_controller = LinearJerkSpeedController::new_simple(
Some(speed_max),
acceleration_max,
jerk_max
);
Self {
mode: Mode::Fixed,
enabled: false,
multiplier: 1.0,
speed: Velocity::ZERO,
strategies: Strategies { fixed, adaptive },
acceleration_controller,
}
}
pub fn update(&mut self, t: Instant)
{
use Mode::*;
let speed = match self.enabled
{
true => match self.mode
{
Fixed => self.strategies.fixed.compute(),
Adaptive => self.strategies.adaptive.compute(),
},
false => Velocity::ZERO,
};
// Apply acceleration control
self.speed = self.acceleration_controller.update(speed * self.multiplier, t);
}
pub fn mode(&self) -> Mode
{
self.mode
}
pub fn set_mode(&mut self, mode: Mode)
{
self.mode = mode;
}
pub fn set_enabled(&mut self, value: bool)
{
self.enabled = value;
}
pub fn speed(&self) -> Velocity
{
self.speed
}
pub fn set_multiplier(&mut self, multiplier: f64)
{
self.multiplier = multiplier;
}
pub fn strategies(&self) -> &Strategies
{
&self.strategies
}
pub fn strategies_mut(&mut self) -> &mut Strategies
{
&mut self.strategies
}
}
#[derive(Debug)]
pub struct Strategies
{
pub fixed: FixedSpeedStrategy,
pub adaptive: AdaptiveSpeedStrategy,
}
#[derive(Debug)]
pub struct FixedSpeedStrategy
{
target_speed: Velocity,
}
impl FixedSpeedStrategy
{
pub fn compute(&self) -> Velocity
{
self.target_speed
}
pub fn target_speed(&self) -> Velocity {
self.target_speed
}
pub fn set_target_speed(&mut self, speed: Velocity) {
self.target_speed = speed;
}
}
#[derive(Debug, Clone, Copy)]
pub struct AdaptiveSpeedStrategy {
base_speed: Velocity,
deviation_max: Velocity,
modulation: f64, // (-1.0 to 1.0)
}
impl AdaptiveSpeedStrategy
{
pub fn compute(&self) -> Velocity
{
self.base_speed + (self.deviation_max * self.modulation)
}
pub fn base_speed(&self) -> Velocity {
self.base_speed
}
pub fn set_base_speed(&mut self, speed: Velocity) {
self.base_speed = speed;
}
pub fn deviation_max(&self) -> Velocity {
self.deviation_max
}
pub fn set_deviation_max(&mut self, deviation: Velocity) {
self.deviation_max = deviation;
}
pub fn set_modulation(&mut self, modulation: f64) {
self.modulation = modulation.clamp(-1.0, 1.0);
}
}
+164
View File
@@ -0,0 +1,164 @@
use std::time::Instant;
use serde::{Deserialize, Serialize};
use units::AngularVelocity;
use control_core::converters::angular_step_converter::AngularStepConverter;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use crate::types::Direction;
use crate::winder2::devices::{ Puller, TensionArm };
use super::OperationState;
mod speed_controller;
use speed_controller::{SpeedController, AdaptiveSpeedController, MinMaxSpeedController};
/// Represents the spool motor
#[derive(Debug)]
pub struct Spool
{
motor: StepperVelocityEL70x1,
operation_state: OperationState,
direction: Direction,
speed_control_mode: SpeedControlMode,
step_converter: AngularStepConverter,
pub speed_controllers: SpeedControllers,
}
// public interface
impl Spool
{
pub fn new(motor: StepperVelocityEL70x1) -> Self
{
Self {
motor,
operation_state: OperationState::Disabled,
direction: Direction::Forward,
speed_controllers: SpeedControllers::new(),
speed_control_mode: SpeedControlMode::Adaptive,
step_converter: AngularStepConverter::new(200),
}
}
pub fn update(&mut self, t: Instant,tension_arm: &TensionArm, puller: &Puller)
{
let multiplier = if self.direction == Direction::Forward { 1.0 } else { -1.0 };
let controller = self.active_controller_mut();
let velocity = controller.update_speed(t, multiplier, tension_arm, puller);
let steps_per_second = self.step_converter.angular_velocity_to_steps(velocity);
_ = self.motor.set_speed(steps_per_second);
}
}
// getter + setter
impl Spool
{
pub fn speed_control_mode(&self) -> SpeedControlMode
{
self.speed_control_mode
}
pub fn set_speed_control_mode(&mut self, mode: SpeedControlMode)
{
if self.speed_control_mode == mode { return; }
self.speed_control_mode = mode;
// get active speed controller
let controller = self.active_controller_mut();
let current_speed = controller.speed();
// Set the speed in the target controller and reset it for smooth transition
controller.set_speed(current_speed);
controller.reset();
controller.set_speed(current_speed);
}
pub fn speed(&self) -> AngularVelocity
{
self.active_controller().speed()
}
pub fn set_operation_state(&mut self, operation_state: OperationState)
{
use OperationState::*;
// No change, nothing to do
if self.operation_state == operation_state { return; }
// Leaving disabled state, enable motor
if self.operation_state == Disabled {
self.motor.set_enabled(true);
}
self.active_controller_mut().set_enabled(operation_state == Running);
self.operation_state = operation_state;
}
pub fn direction(&self) -> Direction
{
self.direction
}
pub fn set_direction(&mut self, direction: Direction)
{
self.direction = direction;
}
}
// utils
impl Spool
{
fn active_controller(&self) -> &dyn SpeedController
{
use SpeedControlMode::*;
match self.speed_control_mode
{
Adaptive => &self.speed_controllers.adaptive,
MinMax => &self.speed_controllers.minmax,
}
}
fn active_controller_mut(&mut self) -> &mut dyn SpeedController
{
use SpeedControlMode::*;
match self.speed_control_mode
{
Adaptive => &mut self.speed_controllers.adaptive,
MinMax => &mut self.speed_controllers.minmax,
}
}
}
// other types
#[derive(Debug)]
pub struct SpeedControllers
{
pub minmax: MinMaxSpeedController,
pub adaptive: AdaptiveSpeedController,
}
impl SpeedControllers
{
pub fn new() -> Self
{
Self {
minmax: MinMaxSpeedController::new(),
adaptive: AdaptiveSpeedController::new()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpeedControlMode
{
Adaptive,
MinMax,
}
@@ -1,9 +1,3 @@
use crate::winder2::{
clamp_revolution::clamp_revolution_uom, filament_tension::FilamentTensionCalculator,
puller_speed_controller::PullerSpeedController,
};
use super::{clamp_revolution::Clamping, tension_arm::TensionArm};
use control_core::{
controllers::first_degree_motion::angular_acceleration_speed_controller::AngularAccelerationSpeedController,
helpers::{interpolation::scale, moving_time_window::MovingTimeWindow},
@@ -18,15 +12,15 @@ use units::f64::*;
use units::length::{centimeter, meter};
use units::velocity::meter_per_second;
/// Adaptive spool speed controller that automatically adjusts to maintain optimal filament tension.
///
/// This controller monitors filament tension via the tension arm and learns the appropriate
/// maximum speed based on puller speed and tension feedback. It uses closed-loop control
/// to minimize tension error and applies smooth acceleration to prevent sudden motor commands.
use crate::{helpers::{Clamp, clamp_revolution_uom}, winder2::devices::spool::speed_controller::SpeedController};
use crate::winder2::devices::{Puller, TensionArm};
use super::helpers::FilamentTensionCalculator;
#[derive(Debug)]
pub struct AdaptiveSpoolSpeedController {
pub struct AdaptiveSpeedController {
/// Last commanded angular velocity sent to the spool motor
last_speed: AngularVelocity,
speed: AngularVelocity,
/// Whether the speed controller is enabled (false = always returns zero speed)
enabled: bool,
/// Acceleration controller to smooth speed transitions and prevent sudden changes
@@ -52,13 +46,9 @@ pub struct AdaptiveSpoolSpeedController {
deacceleration_urgency_multiplier: f64,
}
impl Default for AdaptiveSpoolSpeedController {
fn default() -> Self {
Self::new()
}
}
impl AdaptiveSpoolSpeedController {
// Constants
impl AdaptiveSpeedController
{
/// Maximum speed limit (in RPM) used to initialize the acceleration controller
const INITIAL_MAX_SPEED_RPM: f64 = 150.0;
@@ -87,7 +77,7 @@ impl AdaptiveSpoolSpeedController {
const FACTOR_MAX: f64 = 20.0;
/// if the tension is the lowest, the speed can be up to 2x the puller speed
/// if the tension is the lowest, the speed can be up to X the puller speed
const MAX_SPEED_MULTIPLIER: f64 = 4.0;
/// Base acceleration as a fraction of max possible speed (per second)
@@ -98,18 +88,67 @@ impl AdaptiveSpoolSpeedController {
/// Minimum acceleration limit to prevent completely frozen motion
const MIN_ACCELERATION_LIMIT: f64 = 0.5; // rad/s²
}
/// Creates a new adaptive spool speed controller with default settings.
///
/// Initializes all internal components and sets baseline parameters.
///
/// # Returns
/// A new `AdaptiveSpoolSpeedController` instance ready for use.
pub fn new() -> Self {
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::INITIAL_MAX_SPEED_RPM);
// getter + setter
impl AdaptiveSpeedController
{
// Getters and setters for the new configurable parameters
pub const fn tension_target(&self) -> f64 {
self.tension_target
}
pub const fn set_tension_target(&mut self, value: f64) {
self.tension_target = value.clamp(0.0, 1.0);
}
pub const fn radius_learning_rate(&self) -> f64 {
self.radius_learning_rate
}
pub const fn set_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.radius_learning_rate = radius_learning_rate.max(0.0);
}
pub const fn max_speed_multiplier(&self) -> f64 {
self.max_speed_multiplier
}
pub const fn set_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.max_speed_multiplier = max_speed_multiplier.max(0.1);
}
pub const fn acceleration_factor(&self) -> f64 {
self.acceleration_factor
}
pub const fn set_acceleration_factor(&mut self, acceleration_factor: f64) {
self.acceleration_factor = acceleration_factor.clamp(0.01, 1.0);
}
pub const fn deacceleration_urgency_multiplier(&self) -> f64 {
self.deacceleration_urgency_multiplier
}
pub const fn set_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.deacceleration_urgency_multiplier = deacceleration_urgency_multiplier.max(1.0);
}
}
// public interface
impl AdaptiveSpeedController
{
pub fn new() -> Self
{
use revolution_per_minute as rpm;
let max_speed = AngularVelocity::new::<rpm>(Self::INITIAL_MAX_SPEED_RPM);
Self {
last_speed: AngularVelocity::ZERO,
speed: AngularVelocity::ZERO,
enabled: false,
acceleration_controller: AngularAccelerationSpeedController::new(
Some(AngularVelocity::ZERO),
@@ -135,71 +174,36 @@ impl AdaptiveSpoolSpeedController {
deacceleration_urgency_multiplier: Self::DEACCELERATION_URGENCY_MULTIPLIER,
}
}
}
/// Calculates the current maximum speed based on puller speed and learned factor.
///
/// Uses the fundamental relationship between linear and angular velocity:
/// ω = (v / r) × multiplier
///
/// Where:
/// - ω (omega) = angular velocity in radians per second
/// - v = linear velocity in meters per second
/// - r = radius in meters
/// - multiplier = MAX_SPEED_MULTIPLIER for speed scaling
///
/// This ensures the spool's rim speed is properly scaled relative to the puller's
/// linear speed, with the multiplier allowing for tension control adjustments.
///
/// # Parameters
/// - `puller_speed_controller`: Reference to puller for baseline speed calculation
///
/// # Returns
/// Current maximum angular velocity for the spool in radians per second
fn get_max_speed(&self, puller_speed_controller: &PullerSpeedController) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
(puller_speed_controller.last_speed.get::<meter_per_second>()
/ self.speed_factor.get::<meter>())
* self.max_speed_multiplier,
)
}
/// Calculates the desired spool speed based on filament tension feedback.
///
/// Reads the tension arm angle, converts it to normalized tension, and calculates
/// the appropriate speed. Also updates the learned maximum speed based on tension error.
///
/// # Parameters
/// - `t`: Current timestamp for learning updates
/// - `tension_arm`: Reference to tension arm for angle measurements
/// - `puller_speed_controller`: Reference to puller for baseline speed calculations
///
/// # Returns
/// The calculated target angular velocity for the spool motor
// utils
impl AdaptiveSpeedController
{
fn calculate_speed(
&mut self,
t: Instant,
tension_arm: &TensionArm,
puller_speed_controller: &PullerSpeedController,
puller: &Puller,
) -> AngularVelocity {
let min_speed = AngularVelocity::ZERO;
let max_speed = self.get_max_speed(puller_speed_controller).abs();
let max_speed = self.max_speed(puller).abs();
// Calculate filament tension from arm angle
let tension_arm_angle = tension_arm.get_angle();
let (clamped_angle, clamping_state) = clamp_revolution_uom(
tension_arm_angle,
let tension_arm_revolution = clamp_revolution_uom(
tension_arm.get_angle(),
self.filament_calc.get_max_angle(), // Inverted because min angle = max tension
self.filament_calc.get_min_angle(),
);
// Return minimum speed if tension arm is at limits
if matches!(clamping_state, Clamping::Min | Clamping::Max) {
if matches!(tension_arm_revolution.clamp, Clamp::Min | Clamp::Max)
{
return min_speed;
}
// 1.0 means maximum tension (high angle, low speed)
// 0.0 means minimum tension (low angle, high speed)
let filament_tension = self.filament_calc.calc_filament_tension(clamped_angle);
let filament_tension = self.filament_calc.calc_filament_tension(tension_arm_revolution.value);
self.update_speed_factor(filament_tension, t);
@@ -212,27 +216,15 @@ impl AdaptiveSpoolSpeedController {
))
}
/// Simplified urgency-weighted acceleration that adapts to current operating conditions.
///
/// This approach uses the current max speed (calculated from puller speed and learned diameter)
/// as the basis for acceleration limits, ensuring smooth operation across all speed ranges
/// while providing instant deceleration capability when approaching zero.
///
/// # Parameters
/// - `target_speed`: Target speed to accelerate towards
/// - `puller_speed_controller`: Reference to puller for current max speed calculation
/// - `t`: Current timestamp for the acceleration controller
///
/// # Returns
/// Smoothly accelerated speed with urgency-weighted limits
fn accelerate_speed(
&mut self,
target_speed: AngularVelocity,
puller_speed_controller: &PullerSpeedController,
puller: &Puller,
t: Instant,
) -> AngularVelocity {
) -> AngularVelocity
{
let target_speed_rad_s = target_speed.get::<radian_per_second>();
let current_max_speed = self.get_max_speed(puller_speed_controller);
let current_max_speed = self.max_speed(puller);
let max_speed_rad_s = current_max_speed.get::<radian_per_second>();
// Base acceleration proportional to current max operating speed
@@ -253,48 +245,31 @@ impl AdaptiveSpoolSpeedController {
AngularAcceleration::new::<radian_per_second_squared>(acceleration_limit);
// Update acceleration controller limits
self.acceleration_controller
.set_max_acceleration(acceleration);
self.acceleration_controller
.set_min_acceleration(-acceleration);
self.acceleration_controller.set_max_acceleration(acceleration);
self.acceleration_controller.set_min_acceleration(-acceleration);
let new_speed = self.acceleration_controller.update(target_speed, t);
// Record for diagnostics
self.speed_time_window
.update(new_speed.get::<radian_per_second>(), t);
self.speed_time_window.update(new_speed.get::<radian_per_second>(), t);
new_speed
}
/// Safety function that enforces absolute speed limits.
///
/// Ensures the spool speed never exceeds safe operational limits to protect hardware.
/// Acts as a final safety check after all other speed calculations.
///
/// # Parameters
/// - `speed`: The speed to be safety-checked and clamped
///
/// # Returns
/// Speed guaranteed to be within safe operational limits
fn clamp_speed(&self, speed: AngularVelocity) -> AngularVelocity {
fn clamp_speed(&self, speed: AngularVelocity) -> AngularVelocity
{
use revolution_per_minute as rpm;
let min_speed = AngularVelocity::ZERO;
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::SAFETY_MAX_SPEED_RPM);
let max_speed = AngularVelocity::new::<rpm>(Self::SAFETY_MAX_SPEED_RPM);
speed.max(min_speed).min(max_speed)
}
/// Adaptive learning algorithm that adjusts maximum speed factor based on tension feedback.
///
/// Uses proportional control to adjust the speed factor based on tension error.
/// The controller learns over time to maintain optimal tension by scaling the
/// baseline speed derived from puller speed.
///
/// # Parameters
/// - `filament_tension`: Normalized tension value
/// - `t`: Current timestamp for time-aware learning rate
fn update_speed_factor(&mut self, filament_tension: f64, t: Instant) {
let delta_t = match self.last_max_speed_factor_update {
fn update_speed_factor(&mut self, filament_tension: f64, t: Instant)
{
let delta_t = match self.last_max_speed_factor_update
{
Some(last_update) => t.duration_since(last_update).as_secs_f64(),
None => {
// First call, initialize and return early
@@ -320,66 +295,65 @@ impl AdaptiveSpoolSpeedController {
self.last_max_speed_factor_update = Some(t);
}
/// Main update function that orchestrates the complete speed control pipeline.
///
/// This is the primary interface that calculates the target speed, applies enable/disable
/// logic, smooths speed changes, and applies safety limits.
///
/// Call this function every control loop iteration to get the commanded spool speed.
///
/// # Parameters
/// - `t`: Current timestamp for time-based calculations
/// - `tension_arm`: Reference to tension arm for feedback
/// - `puller_speed_controller`: Reference for adaptive speed scaling
///
/// # Returns
/// Final commanded angular velocity for the spool motor
pub fn update_speed(
&mut self,
t: Instant,
tension_arm: &TensionArm,
puller_speed_controller: &PullerSpeedController,
) -> AngularVelocity {
let target_speed = self.calculate_speed(t, tension_arm, puller_speed_controller);
fn max_speed(&self, puller: &Puller) -> AngularVelocity
{
let puller_speed = puller.output_speed().get::<meter_per_second>();
let speed_factor = self.speed_factor.get::<meter>();
let enabled_speed = if self.enabled {
target_speed
} else {
AngularVelocity::ZERO
};
let speed = (puller_speed / speed_factor) * self.max_speed_multiplier;
let accelerated_speed = self.accelerate_speed(enabled_speed, puller_speed_controller, t);
AngularVelocity::new::<radian_per_second>(speed)
}
}
// Store speed before clamping to preserve the actual commanded value
self.last_speed = accelerated_speed;
self.clamp_speed(accelerated_speed)
impl SpeedController for AdaptiveSpeedController
{
fn speed(&self) -> AngularVelocity
{
self.speed
}
/// Enables or disables the speed controller.
///
/// When disabled, the controller returns zero speed regardless of tension feedback.
///
/// # Parameters
/// - `enabled`: True to enable speed control, false to disable
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
fn set_speed(&mut self, value: AngularVelocity)
{
self.speed = value;
self.acceleration_controller.reset(value);
}
/// Returns whether the speed controller is currently enabled.
///
/// # Returns
/// True if the controller is enabled, false if disabled
pub const fn is_enabled(&self) -> bool {
fn is_enabled(&self) -> bool
{
self.enabled
}
/// Resets the controller to initial state, clearing all learned parameters.
///
/// Use this when starting a new winding operation or after significant
/// process changes that would invalidate learned parameters.
pub fn reset(&mut self) {
self.last_speed = AngularVelocity::ZERO;
fn set_enabled(&mut self, value: bool)
{
self.enabled = value;
}
fn update_speed(
&mut self,
t: Instant,
multiplier: f64,
tension_arm: &TensionArm,
puller: &Puller
) -> AngularVelocity
{
let speed = match self.enabled
{
true => self.calculate_speed(t, tension_arm, puller),
false => AngularVelocity::ZERO,
};
let accelerated_speed = self.accelerate_speed(speed * multiplier, puller, t);
// Store speed before clamping to preserve the actual commanded value
self.speed = accelerated_speed;
self.clamp_speed(accelerated_speed)
}
fn reset(&mut self)
{
self.speed = AngularVelocity::ZERO;
self.acceleration_controller.reset(AngularVelocity::ZERO);
self.speed_factor = Length::new::<centimeter>(4.25);
self.last_max_speed_factor_update = None;
@@ -389,72 +363,4 @@ impl AdaptiveSpoolSpeedController {
self.acceleration_factor = Self::ACCELERATION_FACTOR;
self.deacceleration_urgency_multiplier = Self::DEACCELERATION_URGENCY_MULTIPLIER;
}
/// Returns the last commanded speed from the controller.
///
/// # Returns
/// The angular velocity that was last commanded to the spool motor
pub fn get_speed(&self) -> AngularVelocity {
self.last_speed
}
/// Manually sets the current speed, bypassing normal calculation.
///
/// Use this for manual control or when switching control modes to ensure
/// smooth transitions.
///
/// # Parameters
/// - `speed`: The angular velocity to set as the current speed
pub fn set_speed(&mut self, speed: AngularVelocity) {
self.last_speed = speed;
self.acceleration_controller.reset(speed);
}
pub fn get_speed_factor(&self) -> Length {
self.speed_factor
}
// Getters and setters for the new configurable parameters
pub const fn get_tension_target(&self) -> f64 {
self.tension_target
}
pub const fn set_tension_target(&mut self, tension_target: f64) {
self.tension_target = tension_target.clamp(0.0, 1.0);
}
pub const fn get_radius_learning_rate(&self) -> f64 {
self.radius_learning_rate
}
pub const fn set_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.radius_learning_rate = radius_learning_rate.max(0.0);
}
pub const fn get_max_speed_multiplier(&self) -> f64 {
self.max_speed_multiplier
}
pub const fn set_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.max_speed_multiplier = max_speed_multiplier.max(0.1);
}
pub const fn get_acceleration_factor(&self) -> f64 {
self.acceleration_factor
}
pub const fn set_acceleration_factor(&mut self, acceleration_factor: f64) {
self.acceleration_factor = acceleration_factor.clamp(0.01, 1.0);
}
pub const fn get_deacceleration_urgency_multiplier(&self) -> f64 {
self.deacceleration_urgency_multiplier
}
pub const fn set_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.deacceleration_urgency_multiplier = deacceleration_urgency_multiplier.max(1.0);
}
}
}
@@ -1,9 +1,15 @@
use crate::winder2::{
clamp_revolution::clamp_revolution_uom, filament_tension::FilamentTensionCalculator,
puller_speed_controller::PullerSpeedController,
use std::time::Instant;
use units::{
ConstZero,
f64::*,
angle::degree,
angular_acceleration::{radian_per_second_squared, revolution_per_minute_per_second},
angular_velocity::{radian_per_second, revolution_per_minute, revolution_per_second},
length::meter,
velocity::meter_per_second,
};
use super::{clamp_revolution::Clamping, tension_arm::TensionArm};
use control_core::{
controllers::{
first_degree_motion::angular_acceleration_speed_controller::AngularAccelerationSpeedController,
@@ -14,20 +20,16 @@ use control_core::{
moving_time_window::MovingTimeWindow,
},
};
use std::time::Instant;
use units::ConstZero;
use units::angle::degree;
use units::angular_acceleration::{radian_per_second_squared, revolution_per_minute_per_second};
use units::angular_velocity::{radian_per_second, revolution_per_minute, revolution_per_second};
use units::f64::*;
use units::length::meter;
use units::velocity::meter_per_second;
use crate::{helpers::{Clamp, clamp_revolution_uom}, winder2::devices::{Puller, spool::speed_controller::SpeedController}};
use crate::winder2::devices::TensionArm;
use super::helpers::FilamentTensionCalculator;
#[derive(Debug)]
pub struct MinMaxSpoolSpeedController {
pub struct MinMaxSpeedController
{
/// Current speed in
last_speed: AngularVelocity,
speed: AngularVelocity,
/// Whether the speed controller is enabled or not
enabled: bool,
/// Acceleration controller to dampen speed change
@@ -38,23 +40,14 @@ pub struct MinMaxSpoolSpeedController {
speed_time_window: MovingTimeWindow<f64>,
}
impl Default for MinMaxSpoolSpeedController {
fn default() -> Self {
Self::new()
}
}
impl MinMaxSpoolSpeedController {
/// Parameters:
/// - `min_speed`: Minimum speed
/// - `max_speed`: Maximum speed
/// - `acceleration`: Acceleration
/// - `deceleration`: Deceleration (preferably negative)
pub fn new() -> Self {
impl MinMaxSpeedController
{
pub fn new() -> Self
{
let max_speed = AngularVelocity::new::<revolution_per_minute>(150.0);
Self {
last_speed: AngularVelocity::ZERO,
speed: AngularVelocity::ZERO,
enabled: false,
acceleration_controller: AngularAccelerationSpeedController::new(
Some(AngularVelocity::ZERO),
@@ -75,44 +68,13 @@ impl MinMaxSpoolSpeedController {
}
}
impl MinMaxSpoolSpeedController {
/// Helper method to get min speed without Option type
fn min_speed(&self) -> AngularVelocity {
self.acceleration_controller
.get_min_speed()
.unwrap_or(AngularVelocity::ZERO)
}
/// Helper method to get max speed without Option type
fn max_speed(&self) -> AngularVelocity {
self.acceleration_controller
.get_max_speed()
.unwrap_or(AngularVelocity::new::<radian_per_second>(f64::INFINITY))
}
/// Calculates the desired speed based on the tension arm angle.
///
/// If the arm is over it's maximum angle, the speed is set to the minimum speed.
/// If the arm is under it's minimum angle, the speed is set to the maximum speed.
/// If the arm is within the range, the speed is interpolated between the minimum and maximum speed based on the tension arm angle.
///
/// Parameters:
/// - `t`: The current time.
/// - `tension_arm`: A reference to the `TensionArm` instance that provides the angle of the tension arm.
///
/// Returns:
/// - speed
fn speed_raw(&mut self, _t: Instant, tension_arm: &TensionArm) -> AngularVelocity {
impl MinMaxSpeedController
{
fn speed_raw(&mut self, _t: Instant, tension_arm_angle: Angle) -> AngularVelocity
{
let min_speed = AngularVelocity::ZERO;
// Convert puller speed to angular velocity using a magic factor
// The factor is adjusted so that the tension arm is reasonably high when a standard spool is at its lowest diameter
// Respect the configured max speed limits by taking the minimum of dynamic calculation and configured max
let max_speed = self.max_speed();
// calculate filament tension
let tension_arm_angle = tension_arm.get_angle();
let tension_arm_revolution = clamp_revolution_uom(
tension_arm_angle,
// inverted because min angle is max tension
@@ -120,24 +82,20 @@ impl MinMaxSpoolSpeedController {
self.filament_calc.get_min_angle(),
);
match tension_arm_revolution.1 {
Clamping::Min => return min_speed,
Clamping::Max => return min_speed,
_ => {}
};
// if value was clamped return min speed.
// why? Idk ask guy who left
if matches!(tension_arm_revolution.clamp, Clamp::Min | Clamp::Max) {
return min_speed;
}
let filament_tension = self
.filament_calc
.calc_filament_tension(tension_arm_revolution.0);
.calc_filament_tension(tension_arm_revolution.value);
let filament_tension_inverted = 1.0 - filament_tension;
// use exponetial interpolation to make the speed change more sensitive in the lower range
let filament_tension_exponential = interpolate_exponential(filament_tension_inverted, 2.0);
// interpolate speed linear
// save speed
AngularVelocity::new::<radian_per_second>(scale(
filament_tension_exponential,
min_speed.get::<radian_per_second>(),
@@ -145,14 +103,6 @@ impl MinMaxSpoolSpeedController {
))
}
/// Accelerates the speed using the acceleration controller.
///
/// Parameters:
/// - `speed`: The current speed
/// - `t`: The current time.
///
/// Returns:
/// - The new speed after applying acceleration.
fn accelerate_speed(&mut self, speed: AngularVelocity, t: Instant) -> AngularVelocity {
// The min/mac acceleration depends on the max speed of the last 5secs or the target speed (whatever is higher)
let acceleration = AngularAcceleration::new::<radian_per_second_squared>(
@@ -179,13 +129,6 @@ impl MinMaxSpoolSpeedController {
new_speed
}
/// Clamps the speed to the defined minimum and maximum speed.
///
/// Parameters:
/// - `speed`: The speed to be clamped.
///
/// Returns:
/// - The clamped speed, ensuring it is within the range of `min_speed` and `max_speed`.
fn clamp_speed(&mut self, speed: AngularVelocity) -> AngularVelocity {
let min_speed = self.min_speed();
let max_speed = self.max_speed();
@@ -200,34 +143,8 @@ impl MinMaxSpoolSpeedController {
}
}
impl MinMaxSpoolSpeedController {
pub fn update_speed(&mut self, t: Instant, tension_arm: &TensionArm) -> AngularVelocity {
let speed = self.speed_raw(t, tension_arm);
let speed = match self.enabled {
true => speed,
false => AngularVelocity::ZERO,
};
let speed = self.accelerate_speed(speed, t);
// save speed before clamping or it will stay 0.0
self.last_speed = speed;
self.clamp_speed(speed)
}
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub const fn is_enabled(&self) -> bool {
self.enabled
}
pub fn reset(&mut self) {
self.last_speed = AngularVelocity::ZERO;
self.acceleration_controller.reset(AngularVelocity::ZERO);
}
impl MinMaxSpeedController
{
fn update_acceleration(&mut self) -> Result<(), MotionControllerError> {
// Set acceleration to 1/4 of the range between min and max speed
// The spool will accelerate from min to max speed in 4 seconds
@@ -262,32 +179,36 @@ impl MinMaxSpoolSpeedController {
Ok(())
}
pub fn get_max_speed(&self) -> AngularVelocity {
self.max_speed()
pub fn max_speed(&self) -> AngularVelocity {
self.acceleration_controller
.get_max_speed()
.unwrap_or(AngularVelocity::ZERO)
}
pub fn get_min_speed(&self) -> AngularVelocity {
self.min_speed()
pub fn min_speed(&self) -> AngularVelocity {
self.acceleration_controller
.get_min_speed()
.unwrap_or(AngularVelocity::ZERO)
}
pub fn get_speed(&self) -> AngularVelocity {
self.last_speed
self.speed
}
pub fn set_speed(&mut self, speed: AngularVelocity) {
self.last_speed = speed;
self.speed = speed;
// Also update the acceleration controller's current speed to ensure smooth transitions
self.acceleration_controller.reset(speed);
}
/// derive the radius from the puller speed and the current angular speed
pub fn get_radius(&self, puller_speed_controller: &PullerSpeedController) -> Length {
let puller_speed: Velocity = puller_speed_controller.get_target_speed();
let angular_speed: AngularVelocity = self.last_speed;
pub fn get_radius(&self, puller: &Puller) -> Length
{
let puller_speed = puller.output_speed().get::<meter_per_second>();
let angular_speed = self.speed.get::<revolution_per_second>();
// Calculate the radius using the formula: radius = speed / angular_speed
let radius =
puller_speed.get::<meter_per_second>() / angular_speed.get::<revolution_per_second>();
let radius = puller_speed / angular_speed;
// Ensure the radius is a normal number, otherwise default to 0.0
let radius = Some(radius).filter(|&n| n.is_normal()).unwrap_or(0.0);
@@ -295,3 +216,54 @@ impl MinMaxSpoolSpeedController {
Length::new::<meter>(radius)
}
}
impl SpeedController for MinMaxSpeedController
{
fn speed(&self) -> AngularVelocity {
self.speed
}
fn set_speed(&mut self, speed: AngularVelocity) {
self.speed = speed;
// Also update the acceleration controller's
// current speed to ensure smooth transitions
self.acceleration_controller.reset(speed);
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
fn is_enabled(&self) -> bool {
self.enabled
}
fn update_speed(
&mut self,
t: Instant,
multiplier: f64,
tension_arm: &TensionArm,
puller: &Puller
) -> AngularVelocity
{
_ = puller;
let speed = self.speed_raw(t, tension_arm.get_angle());
let speed = match self.enabled
{
true => speed,
false => AngularVelocity::ZERO,
};
let speed = self.accelerate_speed(speed * multiplier, t);
// save speed before clamping or it will stay 0.0
self.speed = speed;
self.clamp_speed(speed)
}
fn reset(&mut self) {
self.speed = AngularVelocity::ZERO;
self.acceleration_controller.reset(AngularVelocity::ZERO);
}
}
@@ -0,0 +1,32 @@
use std::time::Instant;
use units::AngularVelocity;
mod helpers;
mod min_max;
pub use min_max::MinMaxSpeedController;
mod adapative;
pub use adapative::AdaptiveSpeedController;
use crate::winder2::devices::{Puller, TensionArm};
pub trait SpeedController
{
fn speed(&self) -> AngularVelocity;
fn set_speed(&mut self, speed: AngularVelocity);
fn is_enabled(&self) -> bool;
fn set_enabled(&mut self, enabled: bool);
fn reset(&mut self);
fn update_speed(
&mut self,
t: Instant,
multiplier: f64,
tension_arm: &TensionArm,
puller: &Puller
) -> AngularVelocity;
}
@@ -1,69 +1,83 @@
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
use units::angle::revolution;
use units::electric_potential::volt;
use units::f64::*;
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
#[derive(Debug)]
pub struct TensionArm {
pub analog_input: AnalogInput,
pub zero: Angle,
/// was zeroed at least once
pub zeroed: bool,
pub struct TensionArm
{
sensor: AnalogInput,
zero_offset: Angle,
is_calibrated: bool,
}
impl TensionArm {
pub fn new(analog_input: AnalogInput) -> Self {
// public interface
impl TensionArm
{
pub fn new(sensor: AnalogInput) -> Self
{
Self {
analog_input,
zero: Angle::new::<revolution>(0.0),
zeroed: false,
sensor,
zero_offset: Angle::new::<revolution>(0.0),
is_calibrated: false,
}
}
fn volts_to_angle(&self, volts: f64) -> Angle {
// 0V = 0deg 5V = 3600deg
// always wrap into 0..1 revolution
Angle::new::<revolution>(volts / 5.0) % Angle::new::<revolution>(1.0)
pub fn calibrate(&mut self)
{
self.zero_offset = self.raw_angle();
self.is_calibrated = true;
}
fn get_volts(&self) -> f64 {
// get the normalized value from the analog input
let value = self.analog_input.get_physical();
match value {
AnalogInputValue::Potential(v) => v.get::<volt>(),
_ => panic!("Expected a potential value"),
}
pub const fn is_calibrated(&self) -> bool
{
self.is_calibrated
}
fn raw_angle(&self) -> Angle {
// get volts
let volts = self.get_volts();
// 0V = 0deg 5V = 3600deg
self.volts_to_angle(volts)
}
pub fn get_angle(&self) -> Angle {
pub fn get_angle(&self) -> Angle
{
// revolution is maping -1/1 to 0/1
let raw = self.raw_angle();
// Handle the wraparound case
if raw < self.zero {
if raw < self.zero_offset {
// We've wrapped around, so add a full revolution
(raw + Angle::new::<revolution>(1.0)) - self.zero
(raw + Angle::new::<revolution>(1.0)) - self.zero_offset
} else {
// Normal case
raw - self.zero
raw - self.zero_offset
}
}
pub fn zero(&mut self) {
self.zero = self.raw_angle();
self.zeroed = true;
}
}
// utils
impl TensionArm
{
fn volts_to_angle(&self, volts: f64) -> Angle
{
// 0V = 0deg -> 5V = 3600deg
// always wrap into 0..1 revolution
Angle::new::<revolution>(volts / 5.0) % Angle::new::<revolution>(1.0)
}
fn get_volts(&self) -> f64
{
// TODO: reconsider panicking, to allow system to shutdown gracefully.
// but since I am only moving code, I will not touch this...
let AnalogInputValue::Potential(potential) =
self.sensor.get_physical() else { panic!("Expected a potential value"); };
potential.get::<volt>()
}
fn raw_angle(&self) -> Angle
{
self.volts_to_angle(self.get_volts())
}
}
// tests
#[cfg(test)]
mod tests {
use super::*;
@@ -73,7 +87,6 @@ mod tests {
analog_input::{AnalogInputInput, physical::AnalogInputRange},
analog_input_dummy::AnalogInputDummy,
};
use std::i16;
#[test]
fn volts_to_angle() {
@@ -130,7 +143,7 @@ mod tests {
normalized: (5.0 / 10.0),
wiring_error: false,
});
let physical = tension_arm.analog_input.get_physical();
let physical = tension_arm.sensor.get_physical();
match physical {
AnalogInputValue::Potential(v) => {
assert_relative_eq!(v.get::<volt>(), 5.0, epsilon = f64::EPSILON);
+649
View File
@@ -0,0 +1,649 @@
use std::time::Instant;
use control_core::converters::linear_step_converter::LinearStepConverter;
use ethercat_hal::io::digital_input::DigitalInput;
use ethercat_hal::io::digital_output::DigitalOutput;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use units::AngularVelocity;
use units::ConstZero;
use units::Length;
use units::Velocity;
use units::angular_velocity::revolution_per_second;
use units::length::millimeter;
use units::velocity::millimeter_per_second;
use crate::winder2::devices::Spool;
use super::OperationState;
/// Represents the puller motor
#[derive(Debug)]
pub struct Traverse
{
// hardware
motor: StepperVelocityEL70x1,
laser_pointer: DigitalOutput,
limit_switch: DigitalInput,
// config
operation_state: OperationState,
position: Length,
limit_inner: Length,
limit_outer: Length,
step_size: Length,
padding: Length,
state: State,
// converters
fullstep_converter: LinearStepConverter,
microstep_converter: LinearStepConverter,
}
// constants
impl Traverse
{
const DEFAULT_LIMIT_INNER: f64 = 22.0; // in mm
const DEFAULT_LIMIT_OUTER: f64 = 92.0; // in mm
const DEFAULT_PADDING: f64 = 0.88; // in mm
const DEFAULT_STEP_SIZE: f64 = 1.75; // in mm
const CIRCUMFERENCE: f64 = 35.0; // in mm
const STEPS_PER_REVOLUTION: i16 = 200;
const MICRO_STEPS_COUNT: i16 = 64;
}
// public interface
impl Traverse
{
pub fn new(
motor: StepperVelocityEL70x1,
limit_switch: DigitalInput,
laser_pointer: DigitalOutput,
) -> Self
{
let limit_inner = Length::new::<millimeter>(Self::DEFAULT_LIMIT_INNER);
let limit_outer = Length::new::<millimeter>(Self::DEFAULT_LIMIT_OUTER);
let circumference = Length::new::<millimeter>(Self::CIRCUMFERENCE);
let fullstep_converter = LinearStepConverter::from_circumference(
Self::STEPS_PER_REVOLUTION,
circumference,
);
let microstep_converter = LinearStepConverter::from_circumference(
Self::STEPS_PER_REVOLUTION * Self::MICRO_STEPS_COUNT,
circumference,
);
let step_size = Length::new::<millimeter>(Self::DEFAULT_STEP_SIZE);
let padding = Length::new::<millimeter>(Self::DEFAULT_PADDING);
Self {
operation_state: OperationState::Disabled,
motor,
laser_pointer,
limit_switch,
fullstep_converter,
microstep_converter,
limit_inner,
limit_outer,
step_size,
padding,
position: Length::ZERO,
state: State::NotHomed,
}
}
/// update the traverse. Returns true if the state
/// changed from this update
pub fn update(&mut self, spool: &Spool)
{
if self.operation_state == OperationState::Disabled { return; }
self.update_position();
self.update_state();
let steps_per_second = self.compute_output_steps(spool.speed());
_ = self.motor.set_speed(steps_per_second);
}
}
// getters + setters
impl Traverse
{
pub fn set_operation_state(&mut self, device_state: OperationState)
{
use OperationState::*;
// No change, nothing to do
if self.operation_state == device_state { return; }
// Leaving standby, enable motor
if self.operation_state == Disabled
{
self.motor.set_enabled(true);
}
match device_state // guranteed state change
{
Disabled => self.motor.set_enabled(false),
Holding => self.goto_home(),
Running => self.start_traversing(),
}
self.operation_state = device_state;
}
pub fn state(&self) -> State
{
self.state
}
pub fn limit_inner(&self) -> Length { self.limit_inner }
pub fn try_set_limit_inner(&mut self, limit_inner: Length) -> Result<(), ()>
{
// Validate the new inner limit against current outer limit
if !Self::validate_traverse_limits(limit_inner, self.limit_outer)
{
// Don't update if validation fails - keep the current value
return Err(());
}
self.limit_inner = limit_inner;
Ok(())
}
pub fn limit_outer(&self) -> Length { self.limit_outer }
pub fn try_set_limit_outer(&mut self, limit_outer: Length) -> Result<(), ()>
{
// Validate the new outer limit against current inner limit
if !Self::validate_traverse_limits(self.limit_inner, limit_outer)
{
// Don't update if validation fails - keep the current value
return Err(());
}
self.limit_outer = limit_outer;
Ok(())
}
pub fn step_size(&self) -> Length { self.step_size }
pub fn set_step_size(&mut self, step_size: Length) { self.step_size = step_size; }
pub fn padding(&self) -> Length { self.padding }
pub fn set_padding(&mut self, padding: Length) { self.padding = padding; }
pub fn current_position(&self) -> Option<Length>
{
match self.is_homed()
{
true => Some(self.position),
false => None,
}
}
pub fn laser_pointer_enabled(&self) -> bool
{
self.laser_pointer.get()
}
pub fn set_laser_pointer_enabled(&mut self, value: bool)
{
self.laser_pointer.set(value);
}
}
// State management
impl Traverse
{
pub fn can_goto_limit_inner(&self) -> bool
{
self.operation_state == OperationState::Disabled
|| !self.is_homed()
|| self.is_going_in()
|| self.is_going_home()
|| self.is_traversing()
}
pub fn try_goto_limit_inner(&mut self) -> Result<(), ()>
{
if !self.can_goto_limit_inner()
{
return Err(());
}
self.state = State::GoingIn;
Ok(())
}
pub fn can_goto_limit_outer(&self)-> bool
{
self.operation_state == OperationState::Disabled
|| !self.is_homed()
|| self.is_going_out()
|| self.is_going_home()
|| self.is_traversing()
}
pub fn try_goto_limit_outer(&mut self) -> Result<(), ()>
{
if !self.can_goto_limit_outer()
{
return Err(());
}
self.state = State::GoingOut;
Ok(())
}
pub fn can_go_home(&self)-> bool
{
self.operation_state == OperationState::Disabled
|| !self.is_homed()
|| self.is_going_out()
|| self.is_going_home()
|| self.is_traversing()
}
pub fn try_goto_home(&mut self) -> Result<(), ()>
{
if !self.can_go_home()
{
return Err(());
}
self.goto_home();
Ok(())
}
fn goto_home(&mut self)
{
self.state = State::Homing(HomingState::Initialize);
}
fn start_traversing(&mut self)
{
self.state = State::Traversing(TraversingState::GoingOut);
}
#[allow(dead_code)]
pub fn is_idle(&self) -> bool
{
self.state == State::Idle
}
pub fn is_homed(&self) -> bool {
// if not [`State::NotHomed`], then it is homed
!matches!(self.state, State::NotHomed)
}
pub fn is_going_in(&self) -> bool {
self.state == State::GoingIn
}
pub fn is_going_out(&self) -> bool {
self.state == State::GoingOut
}
pub fn is_going_home(&self) -> bool {
matches!(self.state, State::Homing(_))
}
pub fn is_traversing(&self) -> bool {
matches!(self.state, State::Traversing(_))
}
}
// helpers
impl Traverse
{
fn compute_output_steps(&self, spool_speed: AngularVelocity) -> f64
{
let speed = self.veloctity_from_state(spool_speed);
self.fullstep_converter.velocity_to_steps(speed)
}
pub fn update_position(&mut self)
{
let steps = self.motor.get_position() as f64;
self.position = self.microstep_converter.steps_to_distance(steps);
}
fn endstop_triggered(&self) -> bool
{
self.limit_switch.get_value().unwrap_or(false)
}
fn calculate_traverse_speed(spool_speed: AngularVelocity, step_size: Length) -> Velocity
{
let spool_speed = spool_speed.get::<revolution_per_second>();
let step_size = step_size.get::<millimeter>();
// Calculate the traverse speed directly from spool speed and step size
Velocity::new::<millimeter_per_second>(spool_speed * step_size)
}
// Changes the direction of the speed based on the current position and target position
fn speed_to_position(&self, target_position: Length, absolute_speed: Velocity) -> Velocity
{
// If we are over the target position we need to move negative
if self.position > target_position {
-absolute_speed.abs()
} else if self.position < target_position {
absolute_speed.abs()
} else {
Velocity::ZERO
}
}
/// Calculate distance to position
fn distance_to_position(&self, target_position: Length) -> Length
{
(self.position - target_position).abs()
}
// If at inner limit within a tolerance
fn is_at_position(&self, target_position: Length, tolerance: Length) -> bool
{
let upper_tolerance = target_position + tolerance.abs();
let lower_tolerance = target_position - tolerance.abs();
lower_tolerance <= self.position && self.position <= upper_tolerance
}
/// Validates that traverse limits maintain proper constraints:
/// - Inner limit must be smaller than outer limit
/// - At least 0.9mm difference between inner and outer limits
fn validate_traverse_limits(inner: Length, outer: Length) -> bool
{
outer > inner + Length::new::<millimeter>(0.9)
}
}
// state updates
impl Traverse
{
fn update_state(&mut self)
{
use State::*;
match self.state
{
NotHomed | Idle => {}
GoingIn =>
{
// If inner limit is reached
if self.is_at_position(self.limit_inner, Length::new::<millimeter>(0.01))
{
// Put Into Idle
self.state = State::Idle;
}
}
GoingOut =>
{
// If outer limit is reached
if self.is_at_position(self.limit_outer, Length::new::<millimeter>(0.01)) {
// Put Into Idle
self.state = State::Idle;
}
}
Homing(state) => self.update_homing_state(state),
Traversing(state) => self.update_traversing_state(state),
};
}
fn update_homing_state(&mut self, homing_state: HomingState)
{
use HomingState::*;
match homing_state
{
Initialize =>
{
// If endstop is triggered, escape the endstop
if self.endstop_triggered() {
self.state = State::Homing(EscapeEndstop);
} else {
// If endstop is not triggered, move to the endstop
self.state = State::Homing(FindEndstopCoarse);
}
}
EscapeEndstop => {
// Move out until endstop is not triggered anymore
if !self.endstop_triggered() {
self.state = State::Homing(FindEndstopFineDistancing);
}
}
FindEndstopFineDistancing => {
// Move out until endstop is not triggered anymore
if !self.endstop_triggered() {
// Find endstop fine
self.state = State::Homing(FindEndtopFine);
}
}
FindEndtopFine => {
// If endstop is reached change to idle
if self.endstop_triggered() {
// Set poition of traverse to 0
self.motor.set_position(0);
// Put Into Idle
self.state = State::Homing(Validate(Instant::now()));
}
}
FindEndstopCoarse => {
// Move to endstop
if self.endstop_triggered() {
// Move awaiy from endstop
self.state = State::Homing(FindEndstopFineDistancing);
}
}
Validate(instant) => {
// If 100ms have passed check if position is actually 0.0
if instant.elapsed().as_millis() > 100 {
if self.is_at_position(Length::ZERO, Length::new::<millimeter>(0.01)) {
// If position is 0.0, put into idle
self.state = State::Idle;
} else {
// If position is not 0.0, redo homing
self.state = State::Homing(Initialize);
}
}
}
}
}
fn update_traversing_state(&mut self, state: TraversingState)
{
use TraversingState::*;
match state {
GoingOut => {
// If outer limit is reached
if self.position >= self.limit_outer - self.padding {
// Turn around
self.state = State::Traversing(TraversingIn);
}
}
TraversingIn => {
// If inner limit is reached
if self.position <= self.limit_inner + self.padding {
// Turn around
self.state = State::Traversing(TraversingOut);
}
}
TraversingOut => {
// If outer limit is reached
if self.position >= self.limit_outer - self.padding {
// Turn around
self.state = State::Traversing(TraversingIn);
}
}
}
}
}
// velocity computation
impl Traverse
{
fn veloctity_from_state(&self, spool_speed: AngularVelocity) -> Velocity
{
use State::*;
match self.state {
NotHomed => Velocity::ZERO, // Not homed, no movement
Idle => Velocity::ZERO, // No movement in idle state
GoingIn => {
// Move in at a speed of 10-100 mm/s
self.speed_to_position(
self.limit_inner,
match self.distance_to_position(self.limit_inner).abs()
> Length::new::<millimeter>(1.0)
{
true => Velocity::new::<millimeter_per_second>(100.0),
false => Velocity::new::<millimeter_per_second>(10.0),
},
)
},
GoingOut => {
// Move out at a speed of 10-100 mm/s
self.speed_to_position(
self.limit_outer,
match self.distance_to_position(self.limit_outer).abs()
> Length::new::<millimeter>(1.0)
{
true => Velocity::new::<millimeter_per_second>(100.0),
false => Velocity::new::<millimeter_per_second>(10.0),
},
)
}
Homing(state) => Self::velocity_from_homing_state(state),
Traversing(state) => self.velocity_from_traversing_state(state, spool_speed),
}
}
fn velocity_from_homing_state(homing_state: HomingState) -> Velocity
{
use units::velocity::millimeter_per_second as mmps;
use HomingState::*;
match homing_state {
Initialize => Velocity::ZERO,
// Move out at a speed of 10 mm/s
EscapeEndstop => Velocity::new::<mmps>(10.0),
// Move out at a speed of 2 mm/s
FindEndstopFineDistancing => Velocity::new::<mmps>(2.0),
// Move in at a speed of -100 mm/s
FindEndstopCoarse => Velocity::new::<mmps>(-100.0),
// move into the endstop at 2 mm/s
FindEndtopFine => Velocity::new::<mmps>(-2.0),
// We stand still until the validation cooldown has passed
Validate(_) => Velocity::ZERO,
}
}
fn velocity_from_traversing_state(
&self,
traversing_state: TraversingState,
spool_speed: AngularVelocity
) -> Velocity
{
use TraversingState::*;
match traversing_state {
GoingOut => {
// Move out at a speed of 100 mm/s
self.speed_to_position(
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
Velocity::new::<millimeter_per_second>(100.0),
)
}
TraversingIn =>
{
let offset = Length::new::<millimeter>(0.01);
let target_position = self.limit_inner + self.padding - offset;
let absolute_speed = Self::calculate_traverse_speed(spool_speed, self.step_size);
self.speed_to_position(target_position, absolute_speed)
}
TraversingOut =>
{
let offset = Length::new::<millimeter>(0.01);
let target_position = self.limit_outer - self.padding + offset;
let absolute_speed = Self::calculate_traverse_speed(spool_speed, self.step_size);
self.speed_to_position(target_position, absolute_speed)
}
}
}
}
// other types
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum State
{
/// Initial state
NotHomed,
/// Doing nothing
/// Already homed
Idle,
/// Going to inner limit
///
/// After reaching the inner limit, the state will change to [`State::Idle`]
GoingIn,
/// Going to outer limit
///
/// After reaching the outer limit, the state will change to [`State::Idle`]
GoingOut,
/// Homing is in progress
///
/// After homing is done, the state will change to [`State::Idle`]
Homing(HomingState),
/// Move between inner and outer limits
Traversing(TraversingState),
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TraversingState
{
/// Like [`State::GoingOut`] but
/// - will go into [`State::GoingIn`] after reaching the outer limit
GoingOut,
/// Like [`State::GoingIn`] but
/// - will go into [`State::GoingOut`] after reaching the inner limit
/// - speed is synced to spool speed
TraversingIn,
/// Like [`State::GoingOut`] but
/// - will go into [`State::GoingIn`] after reaching the outer limit
/// - speed is synced to spool speed
TraversingOut,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum HomingState
{
/// In this state the traverse is not moving but checks if the endstop si triggered
/// If the endstop is triggered we go into [`HomingState::EscapeEndstop`]
/// If the endstop is not triggered we go into [`HomingState::FindEndstop`]
Initialize,
/// In this state the traverse is moving out away from the endstop until it's not triggered anymore
/// The it goes into [`HomingState::FindEnstopFineDistancing`]
EscapeEndstop,
/// Moving out away from the endstop
/// Then Transition into [`HomingState::FindEndtopFine`]
FindEndstopFineDistancing,
/// In this state the traverse is fast until it reaches the endstop
FindEndstopCoarse,
/// In this state the traverse is moving slowly until it reaches the endstop
FindEndtopFine,
/// In this state we check if th current position is actually 0.0, if not we redo the homing routine
Validate(Instant),
}
-507
View File
@@ -1,507 +0,0 @@
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::PullerRegulationMode;
pub use super::super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
pub use crate::buffer1::BufferV1;
pub use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
SpoolAutomaticActionState, SpoolSpeedControllerState, StateEvent, TensionArmState,
TraverseState, Winder2Events,
};
pub use control_core::socketio::event::BuildEvent;
pub use control_core::socketio::namespace::NamespaceCacheingLogic;
pub use std::time::Instant;
pub use units::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::*,
length::{meter, millimeter},
velocity::meter_per_minute,
};
}
#[cfg(not(feature = "mock-machine"))]
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
impl Winder2 {
/// Implement Spool
/// called by `act`
pub fn sync_spool_speed(&mut self, t: Instant) {
let angular_velocity = self.spool_speed_controller.update_speed(
t,
&self.tension_arm,
&self.puller_speed_controller,
);
// Apply direction based on forward setting
let directed_angular_velocity = if self.spool_speed_controller.get_forward() {
angular_velocity
} else {
-angular_velocity
};
let steps_per_second = self
.spool_step_converter
.angular_velocity_to_steps(directed_angular_velocity);
let _ = self.spool.set_speed(steps_per_second);
}
pub fn stop_or_pull_spool(&mut self, now: Instant) {
if matches!(
self.spool_automatic_action.mode,
SpoolAutomaticActionMode::NoAction
) {
self.calculate_spool_auto_progress_(now);
return;
}
match self.mode {
Winder2Mode::Pull => self.calculate_spool_auto_progress_(now),
Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
_ => {
self.spool_automatic_action.progress_last_check = now;
return;
}
}
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
match self.spool_automatic_action.mode {
SpoolAutomaticActionMode::NoAction => (),
SpoolAutomaticActionMode::Pull => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Pull);
}
SpoolAutomaticActionMode::Hold => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Hold);
}
}
}
}
/// Implement Mode
pub fn set_mode(&mut self, mode: &Winder2Mode) {
let should_update = *mode != Winder2Mode::Wind || self.can_wind();
if should_update {
// all transitions are allowed
self.mode = mode.clone();
// Apply the mode changes to the spool and puller
self.set_spool_mode(mode);
self.set_puller_mode(mode);
self.set_traverse_mode(mode);
}
self.emit_state();
}
/// Implement Traverse
pub fn set_laser(&mut self, value: bool) {
self.laser.set(value);
self.emit_state();
}
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
let new_inner = Length::new::<millimeter>(limit);
let current_outer = self.traverse_controller.get_limit_outer();
// Validate the new inner limit against current outer limit
if !Self::validate_traverse_limits(new_inner, current_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_inner(new_inner);
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
let new_outer = Length::new::<millimeter>(limit);
let current_inner = self.traverse_controller.get_limit_inner();
// Validate the new outer limit against current inner limit
if !Self::validate_traverse_limits(current_inner, new_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_outer(new_outer);
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
let step_size = Length::new::<millimeter>(step_size);
self.traverse_controller.set_step_size(step_size);
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
let padding = Length::new::<millimeter>(padding);
self.traverse_controller.set_padding(padding);
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
if self.can_go_in() {
self.traverse_controller.goto_limit_inner();
}
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
if self.can_go_out() {
self.traverse_controller.goto_limit_outer();
}
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
if self.can_go_home() {
self.traverse_controller.goto_home();
}
self.emit_state();
}
pub fn get_live_values(&self) -> LiveValuesEvent {
let angle_deg = self.tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
// This is done to reduce flicker in the graphs around the zero point
let angle_deg = if angle_deg >= 270.0 {
angle_deg - 360.0
} else {
angle_deg
};
// Calculate puller speed from current motor steps
let steps_per_second = self.puller.get_speed();
let angular_velocity = self
.puller_speed_controller
.converter
.steps_to_angular_velocity(steps_per_second as f64);
let motor_speed = self
.puller_speed_controller
.angular_velocity_to_speed(angular_velocity);
// Divide by gear ratio to get actual puller/material speed
let puller_speed = motor_speed / self.puller_speed_controller.get_gear_ratio().multiplier();
// Calculate spool RPM from current motor steps (always positive regardless of direction)
let spool_rpm = self
.spool_step_converter
.steps_to_angular_velocity(self.spool.get_speed() as f64)
.get::<revolution_per_minute>()
.abs();
LiveValuesEvent {
traverse_position: self
.traverse_controller
.get_current_position()
.map(|x| x.get::<millimeter>()),
puller_speed: puller_speed.get::<meter_per_minute>().abs(),
spool_rpm,
tension_arm_angle: angle_deg,
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
}
}
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
pub fn build_state_event(&mut self) -> StateEvent {
use crate::MachineCrossConnectionState;
let connected_machine = self.connected_machines.get(0);
let ident = match connected_machine {
Some(machine) => Some(machine.ident.clone()),
None => None,
};
let cross_conn = MachineCrossConnectionState {
machine_identification_unique: ident,
is_available: connected_machine.is_some(),
};
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
traverse_state: TraverseState {
limit_inner: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
limit_outer: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
position_in: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
position_out: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
is_going_in: self.traverse_controller.is_going_in(),
is_going_out: self.traverse_controller.is_going_out(),
is_homed: self.traverse_controller.is_homed(),
is_going_home: self.traverse_controller.is_going_home(),
is_traversing: self.traverse_controller.is_traversing(),
laserpointer: self.laser.get(),
step_size: self.traverse_controller.get_step_size().get::<millimeter>(),
padding: self.traverse_controller.get_padding().get::<millimeter>(),
can_go_in: self.can_go_in(),
can_go_out: self.can_go_out(),
can_go_home: self.can_go_home(),
},
puller_state: PullerState {
regulation: self.puller_speed_controller.regulation_mode.clone(),
target_speed: self
.puller_speed_controller
.target_speed
.get::<meter_per_minute>(),
target_diameter: self
.puller_speed_controller
.target_diameter
.get::<millimeter>(),
forward: self.puller_speed_controller.forward,
gear_ratio: self.puller_speed_controller.gear_ratio,
},
mode_state: ModeState {
mode: self.mode.clone().into(),
can_wind: self.can_wind(),
},
tension_arm_state: TensionArmState {
zeroed: self.tension_arm.zeroed,
},
spool_speed_controller_state: SpoolSpeedControllerState {
regulation_mode: self.spool_speed_controller.get_type().clone(),
minmax_min_speed: self
.spool_speed_controller
.get_minmax_min_speed()
.get::<revolution_per_minute>(),
minmax_max_speed: self
.spool_speed_controller
.get_minmax_max_speed()
.get::<revolution_per_minute>(),
adaptive_tension_target: self.spool_speed_controller.get_adaptive_tension_target(),
adaptive_radius_learning_rate: self
.spool_speed_controller
.get_adaptive_radius_learning_rate(),
adaptive_max_speed_multiplier: self
.spool_speed_controller
.get_adaptive_max_speed_multiplier(),
adaptive_acceleration_factor: self
.spool_speed_controller
.get_adaptive_acceleration_factor(),
adaptive_deacceleration_urgency_multiplier: self
.spool_speed_controller
.get_adaptive_deacceleration_urgency_multiplier(),
forward: self.spool_speed_controller.get_forward(),
},
spool_automatic_action_state: SpoolAutomaticActionState {
spool_required_meters: self.spool_automatic_action.target_length.get::<meter>(),
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
},
connected_machine_state: cross_conn,
}
}
pub fn emit_state(&mut self) {
let state_event = self.build_state_event();
let event = state_event.build();
self.namespace.emit(Winder2Events::State(event));
}
/// Apply the mode changes to the spool
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::spool_mode`]
fn set_traverse_mode(&mut self, mode: &Winder2Mode) {
// Convert to `Winder2Mode` to `TraverseMode`
let mode: TraverseMode = mode.clone().into();
// If coming out of standby
if self.traverse_mode == TraverseMode::Standby && mode != TraverseMode::Standby {
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
}
// If going into standby
if mode == TraverseMode::Standby && self.traverse_mode != TraverseMode::Standby {
// If we are going into standby, we need to stop the traverse
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
// Transition matrix
match self.traverse_mode {
TraverseMode::Standby => match mode {
TraverseMode::Standby => {}
TraverseMode::Hold => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Hold`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Wind`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.start_traversing();
}
},
TraverseMode::Hold => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {}
TraverseMode::Traverse => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Wind`]
self.traverse_controller.start_traversing();
}
},
TraverseMode::Traverse => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Hold`]
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {}
},
}
// Update the internal state
self.traverse_mode = mode;
self.emit_state();
}
/// Implement Tension Arm
pub fn tension_arm_zero(&mut self) {
self.tension_arm.zero();
self.emit_live_values(); // For angle update
// For state update
self.emit_state();
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action.mode = mode;
self.emit_state();
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_speed_controller
.set_regulation_mode(puller_regulation_mode);
self.emit_state();
}
/// Set target speed in m/min
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
// Convert m/min to velocity
let target_speed = Velocity::new::<meter_per_minute>(target_speed);
self.puller_speed_controller.set_target_speed(target_speed);
self.emit_state();
}
/// Set target diameter in mm
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
// Convert m/min to velocity
let target_diameter = Length::new::<millimeter>(target_diameter);
self.puller_speed_controller
.set_target_diameter(target_diameter);
self.emit_state();
}
/// Set forward direction
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_speed_controller.set_forward(forward);
self.emit_state();
}
/// Set gear ratio for winding speed
pub fn puller_set_gear_ratio(&mut self, gear_ratio: super::puller_speed_controller::GearRatio) {
self.puller_speed_controller.set_gear_ratio(gear_ratio);
self.emit_state();
}
// Spool Speed Controller API methods
pub fn spool_set_regulation_mode(
&mut self,
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
) {
self.spool_speed_controller.set_type(regulation_mode);
self.emit_state();
}
/// Set minimum speed for minmax mode in RPM
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
let min_speed = AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
tracing::error!("Failed to set spool min speed: {:?}", e);
}
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
let max_speed = AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
tracing::error!("Failed to set spool max speed: {:?}", e);
}
self.emit_state();
}
/// Set tension target for adaptive mode (0.0-1.0)
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
self.spool_speed_controller
.set_adaptive_tension_target(tension_target);
self.emit_state();
}
/// Set radius learning rate for adaptive mode
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.spool_speed_controller
.set_adaptive_radius_learning_rate(radius_learning_rate);
self.emit_state();
}
/// Set max speed multiplier for adaptive mode
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.spool_speed_controller
.set_adaptive_max_speed_multiplier(max_speed_multiplier);
self.emit_state();
}
/// Set acceleration factor for adaptive mode
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.spool_speed_controller
.set_adaptive_acceleration_factor(acceleration_factor);
self.emit_state();
}
/// Set deacceleration urgency multiplier for adaptive mode
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.spool_speed_controller
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
self.emit_state();
}
/// Set forward rotation direction
pub fn spool_set_forward(&mut self, forward: bool) {
self.spool_speed_controller.set_forward(forward);
self.emit_state();
}
}
-57
View File
@@ -1,57 +0,0 @@
use super::Winder2;
use crate::MachineAct;
use crate::MachineMessage;
use crate::MachineValues;
use std::time::{Duration, Instant};
impl MachineAct for Winder2 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.build_state_event())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
()
}
}
}
}
-68
View File
@@ -1,68 +0,0 @@
use super::Winder2;
use crate::{MachineApi, winder2::api::Mutation};
use serde_json::Value;
use std::time::Instant;
impl MachineApi for Winder2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::EnableTraverseLaserpointer(enable) => self.set_laser(enable),
Mutation::SetMode(mode) => self.set_mode(&mode.into()),
Mutation::SetTraverseLimitOuter(limit) => self.traverse_set_limit_outer(limit),
Mutation::SetTraverseLimitInner(limit) => self.traverse_set_limit_inner(limit),
Mutation::SetTraverseStepSize(size) => self.traverse_set_step_size(size),
Mutation::SetTraversePadding(padding) => self.traverse_set_padding(padding),
Mutation::GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
Mutation::GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
Mutation::GotoTraverseHome => self.traverse_goto_home(),
Mutation::SetPullerRegulationMode(regulation) => self.puller_set_regulation(regulation),
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
Mutation::SetPullerTargetDiameter(_) => todo!(),
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
self.spool_set_adaptive_tension_target(value)
}
Mutation::SetSpoolAdaptiveRadiusLearningRate(value) => {
self.spool_set_adaptive_radius_learning_rate(value)
}
Mutation::SetSpoolAdaptiveMaxSpeedMultiplier(value) => {
self.spool_set_adaptive_max_speed_multiplier(value)
}
Mutation::SetSpoolAdaptiveAccelerationFactor(value) => {
self.spool_set_adaptive_acceleration_factor(value)
}
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
}
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
self.set_spool_automatic_required_meters(meters)
}
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
Mutation::SetConnectedMachine(machine_identification_unique) => {
self.set_connected_buffer(machine_identification_unique)
}
Mutation::DisconnectMachine(machine_identification_unique) => {
self.disconnect_buffer(machine_identification_unique)
}
}
Ok(())
}
fn api_event_namespace(
&mut self,
) -> std::option::Option<control_core::socketio::namespace::Namespace> {
self.namespace.namespace.clone()
}
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
}
-254
View File
@@ -1,254 +0,0 @@
use std::time::Instant;
use super::Winder2;
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::winder2::Winder2Mode;
use crate::winder2::api::LiveValuesEvent;
use crate::winder2::api::{ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events};
use crate::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
use crate::winder2::spool_speed_controller::SpoolSpeedControllerType;
use crate::{MACHINE_WINDER_V1, VENDOR_QITECH};
use control_core::socketio::event::BuildEvent;
use control_core::socketio::namespace::NamespaceCacheingLogic;
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WINDER_V1,
};
pub fn stop_or_pull_spool_reset(&mut self, _now: Instant) {}
/// Implement Mode
pub fn set_mode(&mut self, mode: &Winder2Mode) {
self.mode_state = ModeState {
mode: mode.clone().into(),
can_wind: true,
};
self.emit_state();
}
/// Implement Traverse
pub fn set_laser(&mut self, value: bool) {
self.traverse_state.laserpointer = value;
self.emit_state();
}
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
self.traverse_state.limit_inner = limit;
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
self.traverse_state.limit_outer = limit;
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
self.traverse_state.step_size = step_size;
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
self.traverse_state.padding = padding;
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
self.traverse_state.position_in = self.traverse_state.limit_inner;
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
self.traverse_state.position_in = self.traverse_state.limit_outer;
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
self.traverse_state.position_in = 0.0;
self.emit_state();
}
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
traverse_position: Some(0.0),
puller_speed: 0.0,
spool_rpm: 0.0,
tension_arm_angle: 0.0,
spool_progress: 0.0,
}
}
pub fn build_state_event(&mut self) -> StateEvent {
use crate::MachineCrossConnectionState;
let connected_machine = self.connected_machines.get(0);
let ident = match connected_machine {
Some(machine) => Some(machine.ident.clone()),
None => None,
};
let cross_conn = MachineCrossConnectionState {
machine_identification_unique: ident,
is_available: connected_machine.is_some(),
};
StateEvent {
is_default_state: self.is_default_state,
traverse_state: self.traverse_state.clone(),
puller_state: self.puller_state.clone(),
spool_automatic_action_state: self.spool_automatic_action_state.clone(),
mode_state: self.mode_state.clone(),
tension_arm_state: self.tension_arm_state.clone(),
spool_speed_controller_state: self.spool_speed_controller_state.clone(),
connected_machine_state: cross_conn,
}
}
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let event = state.build();
self.namespace.emit(Winder2Events::State(event));
}
/// Apply the mode changes to the spool
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::spool_mode`]
fn set_traverse_mode(&mut self, _mode: &Winder2Mode) {
self.mode_state = ModeState {
mode: crate::winder2::api::Mode::Standby,
can_wind: true,
};
self.emit_state();
}
/// Implement Tension Arm
pub fn tension_arm_zero(&mut self) {
self.tension_arm_state.zeroed = true;
self.emit_state();
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action_state.spool_required_meters = meters;
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action_state
.spool_automatic_action_mode = mode;
self.emit_state();
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_state.regulation = puller_regulation_mode;
self.emit_state();
}
/// Set target speed in m/min
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
self.puller_state.target_speed = target_speed;
self.emit_state();
}
/// Set target diameter in mm
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
self.puller_state.target_diameter = target_diameter;
self.emit_state();
}
/// Set forward direction
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_state.forward = forward;
self.emit_state();
}
/// Set gear ratio for winding speed
pub fn puller_set_gear_ratio(&mut self, gear_ratio: GearRatio) {
self.puller_state.gear_ratio = gear_ratio;
self.emit_state();
}
// Spool Speed Controller API methods
pub fn spool_set_regulation_mode(&mut self, regulation_mode: SpoolSpeedControllerType) {
self.spool_speed_controller_state.regulation_mode = regulation_mode;
self.emit_state();
}
/// Set minimum speed for minmax mode in RPM
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
self.spool_speed_controller_state.minmax_min_speed = min_speed_rpm;
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
self.spool_speed_controller_state.minmax_max_speed = max_speed_rpm;
self.emit_state();
}
/// Set tension target for adaptive mode (0.0-1.0)
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
self.spool_speed_controller_state.adaptive_tension_target = tension_target;
self.emit_state();
}
/// Set radius learning rate for adaptive mode
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.spool_speed_controller_state
.adaptive_radius_learning_rate = radius_learning_rate;
self.emit_state();
}
/// Set max speed multiplier for adaptive mode
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.spool_speed_controller_state
.adaptive_max_speed_multiplier = max_speed_multiplier;
self.emit_state();
}
/// Set acceleration factor for adaptive mode
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.spool_speed_controller_state
.adaptive_acceleration_factor = acceleration_factor;
self.emit_state();
}
/// Set deacceleration urgency multiplier for adaptive mode
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.spool_speed_controller_state
.adaptive_deacceleration_urgency_multiplier = deacceleration_urgency_multiplier;
self.emit_state();
}
/// Set forward rotation direction
pub fn spool_set_forward(&mut self, forward: bool) {
self.spool_speed_controller_state.forward = forward;
self.emit_state();
}
/// implement machine connection
/// set connected buffer
pub fn set_connected_buffer(
&mut self,
_machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
/// disconnect buffer
pub fn disconnect_buffer(
&mut self,
_machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
}
-64
View File
@@ -1,64 +0,0 @@
use std::time::Instant;
pub mod act;
pub mod api;
pub mod mock_emit;
pub mod new;
use super::api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState, TensionArmState,
TraverseState, Winder2Namespace,
};
use crate::{
AsyncThreadMessage, Machine, MachineConnection, MachineMessage,
machine_identification::MachineIdentificationUnique,
};
use smol::channel::{Receiver, Sender};
#[derive(Debug)]
pub struct Winder2 {
machine_identification_unique: MachineIdentificationUnique,
namespace: Winder2Namespace,
last_measurement_emit: Instant,
pub is_default_state: bool,
/// traverse state
pub traverse_state: TraverseState,
/// puller state
pub puller_state: PullerState,
/// spool automatic action state and progress
pub spool_automatic_action_state: SpoolAutomaticActionState,
/// mode state
pub mode_state: ModeState,
/// tension arm state
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// Receive from Api or MainThread
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
/// Communicate with main thread
main_sender: Option<Sender<AsyncThreadMessage>>,
/// All currently "connected" Machines
connected_machines: Vec<MachineConnection>,
/// Defaults to limit of 2
max_connected_machines: usize,
}
impl std::fmt::Display for Winder2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Winder2")
}
}
impl Machine for Winder2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
-57
View File
@@ -1,57 +0,0 @@
use crate::{
MachineNewParams, MachineNewTrait,
winder2::api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState,
TensionArmState, TraverseState, Winder2Namespace,
},
};
use super::Winder2;
impl MachineNewTrait for Winder2 {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
// Mock machine can work with either Serial or Ethercat hardware
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
crate::MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
crate::MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let (sender, receiver) = smol::channel::unbounded();
let mut winder_mock_machine = Self {
main_sender: params.main_thread_channel.clone(),
max_connected_machines: 2,
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: Winder2Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: now,
is_default_state: true,
traverse_state: TraverseState::default(),
puller_state: PullerState::default(),
spool_automatic_action_state: SpoolAutomaticActionState::default(),
mode_state: ModeState::default(),
tension_arm_state: TensionArmState::default(),
spool_speed_controller_state: SpoolSpeedControllerState::default(),
connected_machines: vec![],
};
winder_mock_machine.emit_state();
Ok(winder_mock_machine)
}
}
+199 -420
View File
@@ -1,458 +1,237 @@
pub mod act;
pub mod adaptive_spool_speed_controller;
pub mod api;
pub mod clamp_revolution;
pub mod emit;
pub mod filament_tension;
pub mod minmax_spool_speed_controller;
pub mod new;
pub mod puller_speed_controller;
pub mod spool_speed_controller;
pub mod tension_arm;
pub mod traverse_controller;
use std::time::{Duration, Instant};
#[cfg(feature = "mock-machine")]
pub mod mock;
use control_core::socketio::namespace::NamespaceCacheingLogic;
#[cfg(feature = "mock-machine")]
mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
pub use std::time::Instant;
pub use units::f64::Length;
pub use units::length::meter;
}
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
pub use super::api::Winder2Namespace;
pub use super::puller_speed_controller::PullerSpeedController;
pub use super::spool_speed_controller::SpoolSpeedController;
pub use super::tension_arm::TensionArm;
pub use super::traverse_controller::TraverseController;
pub use control_core::converters::angular_step_converter::AngularStepConverter;
pub use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
};
pub use smol::channel::{Receiver, Sender};
pub use smol::lock::RwLock;
pub use std::{fmt::Debug, sync::Weak, time::Instant};
pub use crate::buffer1::BufferV1;
pub use crate::{AsyncThreadMessage, Machine};
pub use units::ConstZero;
pub use units::f64::Length;
pub use units::{length::meter, length::millimeter, velocity::meter_per_second};
}
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
use crate::{
MACHINE_WINDER_V1, MachineConnection, MachineMessage, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
MachineChannel,
MachineWithChannel,
MachineConnection,
MachinesLiveValues,
VENDOR_QITECH,
machine_identification::MachineIdentification
};
mod types;
use types::{Mode, Hardware, SpoolLengthTaskCompletedAction};
mod tasks;
use tasks::SpoolLengthTask;
mod devices;
use devices::{Spool, Traverse, Puller, TensionArm};
mod api;
use api::{LiveValues, State};
mod new;
#[derive(Debug)]
pub struct SpoolAutomaticAction {
pub progress: Length,
progress_last_check: Instant,
pub target_length: Length,
pub mode: SpoolAutomaticActionMode,
}
impl Default for SpoolAutomaticAction {
fn default() -> Self {
SpoolAutomaticAction {
progress: Length::new::<meter>(0.0),
progress_last_check: Instant::now(),
target_length: Length::new::<meter>(0.0),
mode: SpoolAutomaticActionMode::default(),
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl Machine for Winder2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug)]
pub struct Winder2 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
connected_machines: Vec<MachineConnection>,
max_connected_machines: usize,
// drivers
pub traverse: StepperVelocityEL70x1,
pub puller: StepperVelocityEL70x1,
pub spool: StepperVelocityEL70x1,
pub tension_arm: TensionArm,
pub laser: DigitalOutput,
// controllers
pub traverse_controller: TraverseController,
pub traverse_end_stop: DigitalInput,
// socketio
namespace: Winder2Namespace,
last_measurement_emit: Instant,
pub machine_identification_unique: MachineIdentificationUnique,
// mode
pub mode: Winder2Mode,
pub spool_mode: SpoolMode,
pub traverse_mode: TraverseMode,
pub puller_mode: PullerMode,
// control circuit arm/spool
pub spool_speed_controller: SpoolSpeedController,
pub spool_step_converter: AngularStepConverter,
// spool automatic action state
pub spool_automatic_action: SpoolAutomaticAction,
// control circuit puller
pub puller_speed_controller: PullerSpeedController,
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
pub struct Winder2
{
// common machine fields
channel: MachineChannel,
last_emit: Instant,
emitted_default_state: bool,
// machine config
mode: Mode,
on_spool_length_task_complete: SpoolLengthTaskCompletedAction,
// devices
spool: Spool,
puller: Puller,
traverse: Traverse,
tension_arm: TensionArm,
// tasks
spool_length_task: SpoolLengthTask,
// reference machine for the pullers adaptive speed mode
puller_speed_reference_machine: Option<MachineConnection>,
}
#[cfg(not(feature = "mock-machine"))]
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
// public interface
impl Winder2
{
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification
{
vendor: VENDOR_QITECH,
machine: MACHINE_WINDER_V1,
machine: crate::MACHINE_WINDER_V1,
};
/// Validates that traverse limits maintain proper constraints:
/// - Inner limit must be smaller than outer limit
/// - At least 0.9mm difference between inner and outer limits
fn validate_traverse_limits(inner: Length, outer: Length) -> bool {
outer > inner + Length::new::<millimeter>(0.9)
}
pub fn new(channel: MachineChannel, hardware: Hardware) -> Self
{
let traverse = Traverse::new(
hardware.traverse_motor,
hardware.traverse_limit_switch,
hardware.traverse_laser_pointer,
);
pub fn sync_traverse_speed(&mut self) {
self.traverse_controller.update_speed(
&mut self.traverse,
&self.traverse_end_stop,
self.spool_speed_controller.get_speed(),
)
Self {
channel,
last_emit: Instant::now(),
emitted_default_state: false,
mode: Mode::Standby,
spool: Spool::new(hardware.spool_motor),
puller: Puller::new(hardware.puller_motor),
traverse,
tension_arm: TensionArm::new(hardware.tension_arm_sensor),
spool_length_task: SpoolLengthTask::new(),
on_spool_length_task_complete: SpoolLengthTaskCompletedAction::NoAction,
puller_speed_reference_machine: None,
}
}
/// Can wind capability check
pub const fn can_wind(&self) -> bool {
// Check if tension arm is zeroed and traverse is homed
self.tension_arm.zeroed
&& self.traverse_controller.is_homed()
&& !self.traverse_controller.is_going_home()
fn can_wind(&self) -> bool
{
// Check if tension arm is calibrated and traverse is homed
self.tension_arm.is_calibrated()
&& self.traverse.is_homed()
&& !self.traverse.is_going_home()
// TODO(JSE): why the 2 checks for traverse?
}
}
/// Can go to inner limit capability check
pub fn can_go_in(&self) -> bool {
// Check if traverse is homed, not in standby, not traversing
// Allow changing direction (even when going out)
// Disallow when homing is in progress
self.traverse_controller.is_homed()
&& self.traverse_mode != TraverseMode::Standby
&& !self.traverse_controller.is_going_in()
&& !self.traverse_controller.is_going_home()
&& !self.traverse_controller.is_traversing()
&& self.mode != Winder2Mode::Wind
}
// utils
impl Winder2
{
/// Updates the spool length task and changes the
/// machines mode as appropiate
fn update_spool_length_task(&mut self, now: Instant)
{
// TODO(JSE): find out if update_timer can be replaced
// with update_progress(), since speed should be 0
/// Can go to outer limit capability check
pub fn can_go_out(&self) -> bool {
// Check if traverse is homed, not in standby, not traversing
// Allow changing direction (even when going in)
// Disallow when homing is in progress
self.traverse_controller.is_homed()
&& self.traverse_mode != TraverseMode::Standby
&& !self.traverse_controller.is_going_out()
&& !self.traverse_controller.is_going_home()
&& !self.traverse_controller.is_traversing()
&& self.mode != Winder2Mode::Wind
}
// refactor of: stop_or_pull_spool
use SpoolLengthTaskCompletedAction::*;
/// Can go home capability check
pub fn can_go_home(&self) -> bool {
// Check if not in standby, not traversing
// Allow going home even when going in or out
self.traverse_mode != TraverseMode::Standby
&& !self.traverse_controller.is_going_home()
&& !self.traverse_controller.is_traversing()
&& self.mode != Winder2Mode::Wind
}
let velocity = self.puller.output_speed();
/// Apply the mode changes to the spool
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::spool_mode`]
fn set_spool_mode(&mut self, mode: &Winder2Mode) {
// Convert to `Winder2Mode` to `SpoolMode`
let mode: SpoolMode = mode.clone().into();
// Transition matrix
match self.spool_mode {
SpoolMode::Standby => match mode {
SpoolMode::Standby => {}
SpoolMode::Hold => {
// From [`SpoolMode::Standby`] to [`SpoolMode::Hold`]
self.spool.set_enabled(true);
}
SpoolMode::Wind => {
self.spool.set_enabled(true);
// self.spool_speed_controller.reset();
self.spool_speed_controller.set_enabled(true);
}
},
SpoolMode::Hold => match mode {
SpoolMode::Standby => {
// From [`SpoolMode::Hold`] to [`SpoolMode::Standby`]
self.spool.set_enabled(false);
}
SpoolMode::Hold => {}
SpoolMode::Wind => {
// From [`SpoolMode::Hold`] to [`SpoolMode::Wind`]
// self.spool_speed_controller.reset();
self.spool_speed_controller.set_enabled(true);
}
},
SpoolMode::Wind => match mode {
SpoolMode::Standby => {
// From [`SpoolMode::Wind`] to [`SpoolMode::Standby`]
self.spool.set_enabled(false);
self.spool_speed_controller.set_enabled(false);
}
SpoolMode::Hold => {
// From [`SpoolMode::Wind`] to [`SpoolMode::Hold`]
self.spool_speed_controller.set_enabled(false);
}
SpoolMode::Wind => {}
},
// always update progress if no action
if self.on_spool_length_task_complete == NoAction
{
self.spool_length_task.update_progress(now, velocity);
return;
}
// Update the internal state
self.spool_mode = mode;
}
/// Apply the mode changes to the puller
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::puller_mode`]
fn set_puller_mode(&mut self, mode: &Winder2Mode) {
// Convert to `Winder2Mode` to `PullerMode`
let mode: PullerMode = mode.clone().into();
// Transition matrix
match self.puller_mode {
PullerMode::Standby => match mode {
PullerMode::Standby => {}
PullerMode::Hold => {
// From [`PullerMode::Standby`] to [`PullerMode::Hold`]
self.puller.set_enabled(true);
}
PullerMode::Pull => {
// From [`PullerMode::Standby`] to [`PullerMode::Pull`]
self.puller.set_enabled(true);
self.puller_speed_controller.set_enabled(true);
}
},
PullerMode::Hold => match mode {
PullerMode::Standby => {
// From [`PullerMode::Hold`] to [`PullerMode::Standby`]
self.puller.set_enabled(false);
}
PullerMode::Hold => {}
PullerMode::Pull => {
// From [`PullerMode::Hold`] to [`PullerMode::Pull`]
self.puller_speed_controller.set_enabled(true);
}
},
PullerMode::Pull => match mode {
PullerMode::Standby => {
// From [`PullerMode::Pull`] to [`PullerMode::Standby`]
self.puller.set_enabled(false);
self.puller_speed_controller.set_enabled(false);
}
PullerMode::Hold => {
// From [`PullerMode::Pull`] to [`PullerMode::Hold`]
self.puller_speed_controller.set_enabled(false);
}
PullerMode::Pull => {}
},
// don't update progress if no mode to move the puller
// is active...
//
// jse: But why even bother, if it ain't moving anyway?
// if the motor doesn't stop immediately won't we have
// a false value then since we stop counting while
// it slows down??
if self.mode != Mode::Pull && self.mode != Mode::Wind
{
self.spool_length_task.update_timer(now);
return;
}
// Update the internal state
self.puller_mode = mode;
}
self.spool_length_task.update_progress(now, velocity);
pub const fn stop_or_pull_spool_reset(&mut self, now: Instant) {
self.spool_automatic_action.progress = Length::ZERO;
self.spool_automatic_action.progress_last_check = now;
}
if self.spool_length_task.is_complete()
{
let mode = match self.on_spool_length_task_complete
{
Pull => Mode::Pull,
Hold => Mode::Hold,
NoAction => return, // unreachable
};
pub fn calculate_spool_auto_progress_(&mut self, now: Instant) {
// Calculate time elapsed since last progress check (in minutes)
let dt = now
.duration_since(self.spool_automatic_action.progress_last_check)
.as_secs_f64();
// Calculate distance pulled during this time interval
let meters_pulled_this_interval = Length::new::<meter>(
self.puller_speed_controller
.last_speed
.get::<meter_per_second>()
* dt,
);
// Update total meters pulled
self.spool_automatic_action.progress += meters_pulled_this_interval.abs();
self.spool_automatic_action.progress_last_check = now;
}
/// Implement Puller
/// called by `act`
pub fn sync_puller_speed(&mut self, t: Instant) {
let angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
let steps_per_second = self
.puller_speed_controller
.converter
.angular_velocity_to_steps(angular_velocity);
let _ = self.puller.set_speed(steps_per_second);
}
}
#[cfg(feature = "mock-machine")]
pub use mock::Winder2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Winder2Mode {
Standby,
Hold,
Pull,
Wind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpoolMode {
Standby,
Hold,
Wind,
}
impl From<Winder2Mode> for SpoolMode {
fn from(mode: Winder2Mode) -> Self {
match mode {
Winder2Mode::Standby => Self::Standby,
Winder2Mode::Hold => Self::Hold,
Winder2Mode::Pull => Self::Hold,
Winder2Mode::Wind => Self::Wind,
self.spool_length_task.reset(now);
self.set_mode(mode);
}
}
fn emit_state(&mut self)
{
let event = self.get_state();
self.emitted_default_state = true;
self.channel.emit(event);
}
fn emit_live_values(&mut self)
{
let event = self.create_live_values();
self.channel.emit(event);
self.last_emit = Instant::now();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TraverseMode {
Standby,
Hold,
Traverse,
}
// base machine trait
impl MachineWithChannel for Winder2
{
type State = State;
type LiveValues = LiveValues;
impl From<Winder2Mode> for TraverseMode {
fn from(mode: Winder2Mode) -> Self {
match mode {
Winder2Mode::Standby => Self::Standby,
Winder2Mode::Hold => Self::Hold,
Winder2Mode::Pull => Self::Hold,
Winder2Mode::Wind => Self::Traverse,
fn update(&mut self, now: std::time::Instant) -> anyhow::Result<()>
{
let prev_traverse_state = self.traverse.state();
self.spool.update(now, &self.tension_arm, &self.puller);
self.puller.update(now);
self.traverse.update(&self.spool);
// update last since it can mutate mode
self.update_spool_length_task(now);
if prev_traverse_state != self.traverse.state()
{
// traverse state changed, emit state change
self.emit_state();
}
if now.duration_since(self.last_emit) > Duration::from_secs_f64(1.0 / 30.0)
{
self.emit_state();
self.emit_live_values();
}
Ok(())
}
fn get_state(&self) -> State
{
self.create_state()
}
fn get_live_values(&self) -> Option<LiveValues>
{
Some(self.create_live_values())
}
fn mutate(&mut self, value: serde_json::Value) -> anyhow::Result<()>
{
self.handle_mutation(value)
}
fn on_namespace(&mut self)
{
self.emit_state();
}
fn on_receive_live_values(&mut self, live_values: MachinesLiveValues)
{
use crate::MachinesLiveValues::Laser;
#[allow(irrefutable_let_patterns)]
if let Laser(live_values) = live_values
{
_ = live_values;
// TODO: use data to regulate speed. But not idea how, when what?
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PullerMode {
Standby,
Hold,
Pull,
}
impl From<Winder2Mode> for PullerMode {
fn from(mode: Winder2Mode) -> Self {
match mode {
Winder2Mode::Standby => Self::Standby,
Winder2Mode::Hold => Self::Hold,
Winder2Mode::Pull => Self::Pull,
Winder2Mode::Wind => Self::Pull,
}
fn get_machine_channel(&self) -> &MachineChannel
{
&self.channel
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for Winder2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Winder2")
fn get_machine_channel_mut(&mut self) -> &mut MachineChannel
{
&mut self.channel
}
}
#[cfg(not(feature = "mock-machine"))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_traverse_limits() {
// Test case 1: Valid limits with exactly 1.0mm difference (should pass)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(16.0);
assert!(Winder2::validate_traverse_limits(inner, outer));
// Test case 2: Invalid limits with exactly 0.9mm difference (should fail)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.9);
assert!(!Winder2::validate_traverse_limits(inner, outer));
// Test case 3: Invalid limits with less than 0.9mm difference (should fail)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.5);
assert!(!Winder2::validate_traverse_limits(inner, outer));
// Test case 4: Invalid limits where inner equals outer (should fail)
let inner = Length::new::<millimeter>(20.0);
let outer = Length::new::<millimeter>(20.0);
assert!(!Winder2::validate_traverse_limits(inner, outer));
// Test case 5: Invalid limits where inner is greater than outer (should fail)
let inner = Length::new::<millimeter>(25.0);
let outer = Length::new::<millimeter>(20.0);
assert!(!Winder2::validate_traverse_limits(inner, outer));
// Test case 6: Valid limits with large difference (should pass)
let inner = Length::new::<millimeter>(10.0);
let outer = Length::new::<millimeter>(80.0);
assert!(Winder2::validate_traverse_limits(inner, outer));
// Test case 7: Edge case - exactly 0.91mm difference (should pass)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.91);
assert!(Winder2::validate_traverse_limits(inner, outer));
// Test case 8: Edge case - exactly 0.89mm difference (should fail)
let inner = Length::new::<millimeter>(15.0);
let outer = Length::new::<millimeter>(15.89);
assert!(!Winder2::validate_traverse_limits(inner, outer));
}
}
}
+47 -127
View File
@@ -1,62 +1,21 @@
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::api::Winder2Namespace;
pub use super::super::tension_arm::TensionArm;
pub use super::super::{Winder2, Winder2Mode};
pub use crate::winder2::puller_speed_controller::PullerSpeedController;
pub use crate::winder2::spool_speed_controller::SpoolSpeedController;
pub use crate::winder2::traverse_controller::TraverseController;
pub use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
pub use anyhow::Error;
pub use control_core::converters::angular_step_converter::AngularStepConverter;
pub use control_core::converters::linear_step_converter::LinearStepConverter;
use anyhow::Error;
use ethercat_hal::{coe::ConfigurableDevice, devices::{ek1100::{EK1100, EK1100_IDENTITY_A}, el2002::{EL2002, EL2002_IDENTITY_A, EL2002_IDENTITY_B, EL2002Port}, el7031::{EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort, coe::EL7031Configuration, pdo::EL7031PredefinedPdoAssignment}, el7031_0030::{self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort, EL7031_0030StepperPort, coe::EL7031_0030Configuration, pdo::EL7031_0030PredefinedPdoAssignment}, el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port, coe::EL7041_0052Configuration}}, io::{analog_input::AnalogInput, digital_input::DigitalInput, digital_output::DigitalOutput, stepper_velocity_el70x1::StepperVelocityEL70x1}, shared_config::{self, el70x1::{EL70x1OperationMode, StmMotorConfiguration}}};
pub use ethercat_hal::coe::ConfigurableDevice;
pub use ethercat_hal::devices::ek1100::EK1100;
pub use ethercat_hal::devices::el2002::{EL2002, EL2002_IDENTITY_B, EL2002Port};
pub use ethercat_hal::devices::el7031::coe::EL7031Configuration;
pub use ethercat_hal::devices::el7031::pdo::EL7031PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031::{
EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort,
};
pub use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
pub use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031_0030::{
self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort,
EL7031_0030StepperPort,
};
pub use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
pub use ethercat_hal::devices::el7041_0052::{
EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port,
};
pub use ethercat_hal::devices::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
pub use ethercat_hal::io::analog_input::AnalogInput;
pub use ethercat_hal::io::digital_input::DigitalInput;
pub use ethercat_hal::io::digital_output::DigitalOutput;
pub use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
pub use ethercat_hal::shared_config;
pub use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
pub use std::time::Instant;
pub use units::ConstZero;
pub use units::f64::*;
pub use units::length::{centimeter, meter, millimeter};
pub use units::velocity::meter_per_minute;
}
use crate::{MachineChannel, get_ethercat_device, winder2::types::Hardware};
pub use crate::{
MachineNewHardware,
MachineNewParams,
MachineNewTrait,
validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
#[cfg(not(feature = "mock-machine"))]
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
use crate::get_ethercat_device;
#[cfg(not(feature = "mock-machine"))]
impl MachineNewTrait for Winder2 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
use super::Winder2;
impl MachineNewTrait for Winder2
{
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error>
{
let device_identification = params.device_group.to_vec();
validate_same_machine_identification_unique(&device_identification)?;
@@ -72,10 +31,9 @@ impl MachineNewTrait for Winder2 {
}
};
// using block_on because making this funciton async creates a lifetime issue
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
// so we can't drop subdevices unless this machine is dropped, which is bad
smol::block_on(async {
// map the required hardware devices
let (el2002, el7041, el7031, el7031_0030) = smol::block_on(async
{
// Role 0: Buscoupler EK1100
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?;
@@ -86,9 +44,7 @@ impl MachineNewTrait for Winder2 {
params,
1,
vec![EL2002_IDENTITY_A, EL2002_IDENTITY_B],
)
.await?
.0;
).await?.0;
// Role 2: Stepper Spool EL7041-0052
let el7041 = {
@@ -118,6 +74,7 @@ impl MachineNewTrait for Winder2 {
.await
.write_config(&device.1, &el7041_config)
.await?;
device.0
};
@@ -184,73 +141,36 @@ impl MachineNewTrait for Winder2 {
.await
.write_config(&device.1, &el7031_0030_config)
.await?;
device.0
};
let mode = Winder2Mode::Standby;
Ok::<_, anyhow::Error>((el2002, el7041, el7031, el7031_0030))
})?;
let machine_id = params
.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone();
let (sender, receiver) = smol::channel::unbounded();
let mut new = Self {
main_sender: params.main_thread_channel.clone(),
max_connected_machines: 2,
api_receiver: receiver,
api_sender: sender,
traverse: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
traverse_end_stop: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
puller: StepperVelocityEL70x1::new(
el7031_0030.clone(),
EL7031_0030StepperPort::STM1,
),
spool: StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
tension_arm: TensionArm::new(AnalogInput::new(
el7031_0030,
EL7031_0030AnalogInputPort::AI1,
)),
laser: DigitalOutput::new(el2002, EL2002Port::DO1),
namespace: Winder2Namespace {
namespace: params.namespace.clone(),
},
mode: mode.clone(),
spool_step_converter: AngularStepConverter::new(200),
spool_speed_controller: SpoolSpeedController::new(),
last_measurement_emit: Instant::now(),
spool_mode: mode.clone().into(),
traverse_mode: mode.clone().into(),
puller_mode: mode.into(),
puller_speed_controller: PullerSpeedController::new(
Velocity::new::<meter_per_minute>(1.0),
Length::new::<millimeter>(1.75),
LinearStepConverter::from_diameter(
200, // Assuming 200 steps per revolution for the puller stepper,
Length::new::<centimeter>(8.0), // 8cm diameter of the puller wheel
),
),
traverse_controller: TraverseController::new(
Length::new::<millimeter>(22.0), // Default inner limit
Length::new::<millimeter>(92.0), // Default outer limit
64, // Microsteps
),
emitted_default_state: false,
spool_automatic_action: super::SpoolAutomaticAction {
progress: Length::ZERO,
progress_last_check: Instant::now(),
target_length: Length::new::<meter>(250.0),
mode: super::api::SpoolAutomaticActionMode::NoAction,
},
machine_identification_unique: machine_id,
connected_machines: vec![],
};
let machine_id = params
.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone();
// initalize events
new.emit_state();
Ok(new)
})
let channel = MachineChannel::new(machine_id);
let hardware = Hardware {
spool_motor: StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
traverse_motor: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
traverse_limit_switch: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
puller_motor: StepperVelocityEL70x1::new(el7031_0030.clone(), EL7031_0030StepperPort::STM1),
tension_arm_sensor: AnalogInput::new(el7031_0030, EL7031_0030AnalogInputPort::AI1),
traverse_laser_pointer: DigitalOutput::new(el2002, EL2002Port::DO1),
};
let mut instance = Self::new(channel, hardware);
instance.emit_state();
Ok(instance)
}
}
}
+20
View File
@@ -0,0 +1,20 @@
removed features:
----------------------------------------------------------------------------------------------
- removing puller target speed time series to accomodate different speed controllers
detected 'bugs':
----------------------------------------------------------------------------------------------
- SpoolLengthTask updated in pull mode??
- Inverting direction of spool while running can damage motor
- gear ratio inverted
- Standby mode doesn't properly de-accelerate speed as it should for spool and puller
- Hold mode doesn't de-accelerate Spool motor but puller motor
- traverse position jumps into negative
adjustments:
----------------------------------------------------------------------------------------------
- Shift Gear speed change logic into backend from frontend
traverse:
- finally dont allow to go in/out in UI
-
@@ -1,155 +0,0 @@
use std::time::Instant;
use control_core::{
controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
converters::linear_step_converter::LinearStepConverter,
};
use serde::{Deserialize, Serialize};
use units::ConstZero;
use units::acceleration::meter_per_minute_per_second;
use units::f64::*;
use units::jerk::meter_per_minute_per_second_squared;
use units::velocity::meter_per_minute;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum GearRatio {
OneToOne,
OneToFive,
OneToTen,
}
impl GearRatio {
/// Get the speed multiplier for this gear ratio
pub fn multiplier(&self) -> f64 {
match self {
GearRatio::OneToOne => 1.0,
GearRatio::OneToFive => 5.0,
GearRatio::OneToTen => 10.0,
}
}
}
impl Default for GearRatio {
fn default() -> Self {
GearRatio::OneToOne
}
}
#[derive(Debug)]
pub struct PullerSpeedController {
enabled: bool,
pub target_speed: Velocity,
pub target_diameter: Length,
pub regulation_mode: PullerRegulationMode,
/// Forward rotation direction. If false, applies negative sign to speed
pub forward: bool,
/// Gear ratio for winding speed (1:5 or 1:10)
pub gear_ratio: GearRatio,
/// Linear acceleration controller to dampen speed change
acceleration_controller: LinearJerkSpeedController,
/// Converter for linear to angular transformations
pub converter: LinearStepConverter,
pub last_speed: Velocity,
}
impl PullerSpeedController {
pub fn new(
target_speed: Velocity,
target_diameter: Length,
converter: LinearStepConverter,
) -> Self {
let acceleration = Acceleration::new::<meter_per_minute_per_second>(5.0);
let jerk = Jerk::new::<meter_per_minute_per_second_squared>(10.0);
let speed = Velocity::new::<meter_per_minute>(50.0);
Self {
enabled: false,
target_speed,
target_diameter,
regulation_mode: PullerRegulationMode::Speed,
forward: true,
gear_ratio: GearRatio::default(),
acceleration_controller: LinearJerkSpeedController::new_simple(
Some(speed),
acceleration,
jerk,
),
converter,
last_speed: Velocity::ZERO,
}
}
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn set_target_speed(&mut self, target: Velocity) {
self.target_speed = target;
}
pub fn set_target_diameter(&mut self, target: Length) {
self.target_diameter = target;
}
pub const fn set_regulation_mode(&mut self, regulation: PullerRegulationMode) {
self.regulation_mode = regulation;
}
pub const fn set_forward(&mut self, forward: bool) {
self.forward = forward;
}
pub const fn set_gear_ratio(&mut self, gear_ratio: GearRatio) {
self.gear_ratio = gear_ratio;
}
pub const fn get_gear_ratio(&self) -> GearRatio {
self.gear_ratio
}
fn update_speed(&mut self, t: Instant) -> Velocity {
let base_speed = match self.enabled {
true => match self.regulation_mode {
PullerRegulationMode::Speed => self.target_speed,
PullerRegulationMode::Diameter => unimplemented!(),
},
false => Velocity::ZERO,
};
// Apply gear ratio multiplier
let speed = base_speed * self.gear_ratio.multiplier();
let speed = if self.forward { speed } else { -speed };
let speed = self.acceleration_controller.update(speed, t);
self.last_speed = speed;
speed
}
pub fn speed_to_angular_velocity(&self, speed: Velocity) -> AngularVelocity {
// Use the converter to transform from linear velocity to angular velocity
self.converter.velocity_to_angular_velocity(speed)
}
pub fn angular_velocity_to_speed(&self, angular_speed: AngularVelocity) -> Velocity {
// Use the converter to transform from angular velocity to linear velocity
self.converter.angular_velocity_to_velocity(angular_speed)
}
pub fn calc_angular_velocity(&mut self, t: Instant) -> AngularVelocity {
let speed = self.update_speed(t);
self.speed_to_angular_velocity(speed)
}
pub fn get_target_speed(&self) -> Velocity {
self.target_speed
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum PullerRegulationMode {
#[default]
Speed,
Diameter,
}
@@ -1,194 +0,0 @@
use crate::winder2::{
adaptive_spool_speed_controller::AdaptiveSpoolSpeedController,
minmax_spool_speed_controller::MinMaxSpoolSpeedController,
puller_speed_controller::PullerSpeedController,
};
use control_core::controllers::second_degree_motion::acceleration_position_controller::MotionControllerError;
use super::tension_arm::TensionArm;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use units::f64::*;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum SpoolSpeedControllerType {
#[default]
Adaptive,
MinMax,
}
#[derive(Debug)]
pub struct SpoolSpeedController {
adaptive_controller: AdaptiveSpoolSpeedController,
minmax_controller: MinMaxSpoolSpeedController,
r#type: SpoolSpeedControllerType,
forward: bool,
}
impl Default for SpoolSpeedController {
fn default() -> Self {
Self::new()
}
}
impl SpoolSpeedController {
pub fn new() -> Self {
Self {
adaptive_controller: AdaptiveSpoolSpeedController::new(),
minmax_controller: MinMaxSpoolSpeedController::new(),
r#type: SpoolSpeedControllerType::Adaptive,
forward: true,
}
}
pub fn get_speed(&self) -> AngularVelocity {
match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.get_speed(),
SpoolSpeedControllerType::MinMax => self.minmax_controller.get_speed(),
}
}
pub fn set_speed(&mut self, speed: AngularVelocity) {
match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.set_speed(speed),
SpoolSpeedControllerType::MinMax => self.minmax_controller.set_speed(speed),
}
}
pub const fn set_enabled(&mut self, enabled: bool) {
self.adaptive_controller.set_enabled(enabled);
self.minmax_controller.set_enabled(enabled);
}
pub const fn is_enabled(&self) -> bool {
match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.is_enabled(),
SpoolSpeedControllerType::MinMax => self.minmax_controller.is_enabled(),
}
}
pub fn set_type(&mut self, r#type: SpoolSpeedControllerType) {
// If we're switching to a different type, copy the current speed to the target controller
if std::mem::discriminant(&self.r#type) != std::mem::discriminant(&r#type) {
// Get the current speed from the active controller
let current_speed = match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.get_speed(),
SpoolSpeedControllerType::MinMax => self.minmax_controller.get_speed(),
};
// Set the speed in the target controller and reset it for smooth transition
match r#type {
SpoolSpeedControllerType::Adaptive => {
self.adaptive_controller.set_speed(current_speed);
self.adaptive_controller.reset();
self.adaptive_controller.set_speed(current_speed); // Set again after reset to maintain speed
}
SpoolSpeedControllerType::MinMax => {
self.minmax_controller.set_speed(current_speed);
self.minmax_controller.reset();
self.minmax_controller.set_speed(current_speed); // Set again after reset to maintain speed
}
}
}
self.r#type = r#type;
}
pub const fn get_type(&self) -> &SpoolSpeedControllerType {
&self.r#type
}
pub fn set_minmax_min_speed(
&mut self,
min_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.minmax_controller.set_min_speed(min_speed)
}
pub fn set_minmax_max_speed(
&mut self,
max_speed: AngularVelocity,
) -> Result<(), MotionControllerError> {
self.minmax_controller.set_max_speed(max_speed)
}
pub fn get_minmax_min_speed(&self) -> AngularVelocity {
self.minmax_controller.get_min_speed()
}
pub fn get_minmax_max_speed(&self) -> AngularVelocity {
self.minmax_controller.get_max_speed()
}
pub fn update_speed(
&mut self,
t: Instant,
tension_arm: &TensionArm,
puller_speed_controller: &PullerSpeedController,
) -> AngularVelocity {
match self.r#type {
SpoolSpeedControllerType::Adaptive => {
self.adaptive_controller
.update_speed(t, tension_arm, puller_speed_controller)
}
SpoolSpeedControllerType::MinMax => self.minmax_controller.update_speed(t, tension_arm),
}
}
// Adaptive controller parameter getters and setters
pub const fn get_adaptive_tension_target(&self) -> f64 {
self.adaptive_controller.get_tension_target()
}
pub const fn set_adaptive_tension_target(&mut self, tension_target: f64) {
self.adaptive_controller.set_tension_target(tension_target);
}
pub const fn get_adaptive_radius_learning_rate(&self) -> f64 {
self.adaptive_controller.get_radius_learning_rate()
}
pub const fn set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.adaptive_controller
.set_radius_learning_rate(radius_learning_rate);
}
pub const fn get_adaptive_max_speed_multiplier(&self) -> f64 {
self.adaptive_controller.get_max_speed_multiplier()
}
pub const fn set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.adaptive_controller
.set_max_speed_multiplier(max_speed_multiplier);
}
pub const fn get_adaptive_acceleration_factor(&self) -> f64 {
self.adaptive_controller.get_acceleration_factor()
}
pub const fn set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.adaptive_controller
.set_acceleration_factor(acceleration_factor);
}
pub const fn get_adaptive_deacceleration_urgency_multiplier(&self) -> f64 {
self.adaptive_controller
.get_deacceleration_urgency_multiplier()
}
pub const fn set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.adaptive_controller
.set_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
}
pub const fn get_forward(&self) -> bool {
self.forward
}
pub const fn set_forward(&mut self, forward: bool) {
self.forward = forward;
}
}
+71
View File
@@ -0,0 +1,71 @@
use std::time::Instant;
use units::{ConstZero, Length, Velocity};
use units::length::meter;
use units::velocity::meter_per_second;
#[derive(Debug)]
pub struct SpoolLengthTask
{
current_length: Length,
target_length: Length,
last_check: Instant,
}
impl SpoolLengthTask
{
pub fn new() -> Self
{
SpoolLengthTask {
target_length: Length::new::<meter>(0.0),
current_length: Length::new::<meter>(0.0),
last_check: Instant::now(),
}
}
pub fn is_complete(&self) -> bool
{
self.current_length >= self.target_length
}
pub const fn current_length(&self) -> Length
{
self.current_length
}
pub const fn target_length(&self) -> Length
{
self.target_length
}
pub fn set_target_length(&mut self, target_length: Length)
{
self.target_length = target_length;
}
pub fn update_timer(&mut self, now: Instant)
{
self.last_check = now;
}
pub fn update_progress(&mut self, now: Instant, velocity: Velocity)
{
// Calculate time elapsed since last progress check (in minutes)
let dt = now.duration_since(self.last_check).as_secs_f64();
// Calculate distance pulled during this time interval
let meters_pulled_this_interval = Length::new::<meter>(
velocity.get::<meter_per_second>() * dt,
);
// Update total meters pulled
self.current_length += meters_pulled_this_interval.abs();
self.last_check = now;
}
pub fn reset(&mut self, now: Instant)
{
self.current_length = Length::ZERO;
self.last_check = now;
}
}
-517
View File
@@ -1,517 +0,0 @@
use std::time::Instant;
use control_core::converters::linear_step_converter::LinearStepConverter;
use ethercat_hal::io::{
digital_input::DigitalInput, stepper_velocity_el70x1::StepperVelocityEL70x1,
};
use units::ConstZero;
use units::angular_velocity::revolution_per_second;
use units::f64::{AngularVelocity, Length, Velocity};
use units::length::millimeter;
use units::velocity::millimeter_per_second;
#[derive(Debug)]
pub struct TraverseController {
enabled: bool,
position: Length,
limit_inner: Length,
limit_outer: Length,
step_size: Length,
padding: Length,
state: State,
fullstep_converter: LinearStepConverter,
microstep_converter: LinearStepConverter,
// A sticky flag if the [`State`] changed (not the sub states)
// Needed to send state updates to the UI
did_change_state: bool,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum State {
/// Initial state
NotHomed,
/// Doing nothing
/// Already homed
Idle,
/// Going to inner limit
///
/// After reaching the inner limit, the state will change to [`State::Idle`]
GoingIn,
/// Going to outer limit
///
/// After reaching the outer limit, the state will change to [`State::Idle`]
GoingOut,
/// Homing is in progress
///
/// After homing is done, the state will change to [`State::Idle`]
Homing(HomingState),
/// Move between inner and outer limits
Traversing(TraversingState),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TraversingState {
/// Like [`State::GoingOut`] but
/// - will go into [`State::GoingIn`] after reaching the outer limit
GoingOut,
/// Like [`State::GoingIn`] but
/// - will go into [`State::GoingOut`] after reaching the inner limit
/// - speed is synced to spool speed
TraversingIn,
/// Like [`State::GoingOut`] but
/// - will go into [`State::GoingIn`] after reaching the outer limit
/// - speed is synced to spool speed
TraversingOut,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum HomingState {
/// In this state the traverse is not moving but checks if the endstop si triggered
/// If the endstop is triggered we go into [`HomingState::EscapeEndstop`]
/// If the endstop is not triggered we go into [`HomingState::FindEndstop`]
Initialize,
/// In this state the traverse is moving out away from the endstop until it's not triggered anymore
/// The it goes into [`HomingState::FindEnstopFineDistancing`]
EscapeEndstop,
/// Moving out away from the endstop
/// Then Transition into [`HomingState::FindEndtopFine`]
FindEndstopFineDistancing,
/// In this state the traverse is fast until it reaches the endstop
FindEndstopCoarse,
/// In this state the traverse is moving slowly until it reaches the endstop
FindEndtopFine,
/// In this state we check if th current position is actually 0.0, if not we redo the homing routine
Validate(Instant),
}
impl TraverseController {
pub fn new(limit_inner: Length, limit_outer: Length, microsteps: u8) -> Self {
Self {
enabled: false,
position: Length::ZERO,
limit_inner,
limit_outer,
step_size: Length::new::<millimeter>(1.75), // Default step size
padding: Length::new::<millimeter>(0.88), // Default padding
state: State::NotHomed,
did_change_state: false,
fullstep_converter: LinearStepConverter::from_circumference(
200,
Length::new::<millimeter>(35.0),
),
microstep_converter: LinearStepConverter::from_circumference(
200 * microsteps as i16,
Length::new::<millimeter>(35.0),
),
}
}
}
// Getter & Setter
impl TraverseController {
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn set_limit_inner(&mut self, limit: Length) {
self.limit_inner = limit;
}
pub fn set_limit_outer(&mut self, limit: Length) {
self.limit_outer = limit;
}
pub fn set_step_size(&mut self, step_size: Length) {
self.step_size = step_size;
}
pub fn set_padding(&mut self, padding: Length) {
self.padding = padding;
}
pub fn get_limit_inner(&self) -> Length {
self.limit_inner
}
pub fn get_limit_outer(&self) -> Length {
self.limit_outer
}
pub fn get_step_size(&self) -> Length {
self.step_size
}
pub fn get_padding(&self) -> Length {
self.padding
}
pub fn get_current_position(&self) -> Option<Length> {
match self.is_homed() {
true => Some(self.position),
false => None,
}
}
pub const fn is_enabled(&self) -> bool {
self.enabled
}
pub const fn did_change_state(&mut self) -> bool {
let did_change = self.did_change_state;
// Reset the flag
self.did_change_state = false;
did_change
}
}
// State management
impl TraverseController {
pub const fn goto_limit_inner(&mut self) {
self.state = State::GoingIn;
}
pub const fn goto_limit_outer(&mut self) {
self.state = State::GoingOut;
}
pub const fn goto_home(&mut self) {
self.state = State::Homing(HomingState::Initialize);
}
pub const fn start_traversing(&mut self) {
self.state = State::Traversing(TraversingState::GoingOut);
}
pub const fn is_homed(&self) -> bool {
// if not [`State::NotHomed`], then it is homed
!matches!(self.state, State::NotHomed)
}
pub const fn is_going_in(&self) -> bool {
// [`State::GoingIn`]
matches!(self.state, State::GoingIn)
}
pub const fn is_going_out(&self) -> bool {
// [`State::GoingOut`]
matches!(self.state, State::GoingOut)
}
pub const fn is_going_home(&self) -> bool {
// [`State::Homing`]
matches!(self.state, State::Homing(_))
}
pub const fn is_traversing(&self) -> bool {
// [`State::Traversing`]
matches!(self.state, State::Traversing(_))
}
}
impl TraverseController {
// If at inner limit within a tolerance
fn is_at_position(&self, target_position: Length, tolerance: Length) -> bool {
let upper_tolerance = target_position + tolerance.abs();
let lower_tolerance = target_position - tolerance.abs();
self.position >= lower_tolerance && self.position <= upper_tolerance
}
/// Calculate distance to position
fn distance_to_position(&self, target_position: Length) -> Length {
if self.position > target_position {
self.position - target_position
} else if self.position < target_position {
target_position - self.position
} else {
Length::ZERO
}
}
// Changes the direction of the speed based on the current position and target position
fn speed_to_position(&self, target_position: Length, absolute_speed: Velocity) -> Velocity {
// If we are over the target position we need to move negative
if self.position > target_position {
-absolute_speed.abs()
} else if self.position < target_position {
absolute_speed.abs()
} else {
Velocity::ZERO
}
}
/// Gets the current traverse position as a [`Length`].
pub fn sync_position(&mut self, traverse: &StepperVelocityEL70x1) {
let steps = traverse.get_position();
self.position = self.microstep_converter.steps_to_distance(steps as f64);
}
/// Update the [`did_change_state`] flag
/// Only considers the major state not the sub states
const fn update_did_change_state(&mut self, old_state: &State) -> bool {
match self.state {
State::NotHomed => !matches!(old_state, State::NotHomed),
State::Idle => !matches!(old_state, State::Idle),
State::GoingIn => !matches!(old_state, State::GoingIn),
State::GoingOut => !matches!(old_state, State::GoingOut),
State::Homing(_) => !matches!(old_state, State::Homing(_)),
State::Traversing(_) => !matches!(old_state, State::Traversing(_)),
}
}
/// Calculates a desired speed based on the current state and the end stop status.
///
/// Positive speed moved out, negative speed moves in.
fn get_speed(
&mut self,
traverse: &mut StepperVelocityEL70x1,
traverse_end_stop: &DigitalInput,
spool_speed: AngularVelocity,
) -> Velocity {
// Don't move if not enabled or in a state that doesn't result in movement
if !self.enabled {
return Velocity::ZERO;
}
self.sync_position(traverse);
// save state before
let old_state = self.state.clone();
// Automatic Transitions
match &self.state {
State::NotHomed => {}
State::Idle => {}
State::GoingIn => {
// If inner limit is reached
if self.is_at_position(self.limit_inner, Length::new::<millimeter>(0.01)) {
// Put Into Idle
self.state = State::Idle;
}
}
State::GoingOut => {
// If outer limit is reached
if self.is_at_position(self.limit_outer, Length::new::<millimeter>(0.01)) {
// Put Into Idle
self.state = State::Idle;
}
}
State::Homing(homing_state) => match homing_state {
HomingState::Initialize => {
// If endstop is triggered, escape the endstop
if traverse_end_stop.get_value().unwrap_or(false) {
self.state = State::Homing(HomingState::EscapeEndstop);
} else {
// If endstop is not triggered, move to the endstop
self.state = State::Homing(HomingState::FindEndstopCoarse);
}
}
HomingState::EscapeEndstop => {
// Move out until endstop is not triggered anymore
if !traverse_end_stop.get_value().unwrap_or(false) {
self.state = State::Homing(HomingState::FindEndstopFineDistancing);
}
}
HomingState::FindEndstopFineDistancing => {
// Move out until endstop is not triggered anymore
if !traverse_end_stop.get_value().unwrap_or(false) {
// Find endstop fine
self.state = State::Homing(HomingState::FindEndtopFine);
}
}
HomingState::FindEndtopFine => {
// If endstop is reached change to idle
if traverse_end_stop.get_value().unwrap_or(false) {
// Set poition of traverse to 0
traverse.set_position(0);
// Put Into Idle
self.state = State::Homing(HomingState::Validate(Instant::now()));
}
}
HomingState::FindEndstopCoarse => {
// Move to endstop
if traverse_end_stop.get_value().unwrap_or(false) {
// Move awaiy from endstop
self.state = State::Homing(HomingState::FindEndstopFineDistancing);
}
}
HomingState::Validate(instant) => {
// If 100ms have passed check if position is actually 0.0
if instant.elapsed().as_millis() > 100 {
if self.is_at_position(Length::ZERO, Length::new::<millimeter>(0.01)) {
// If position is 0.0, put into idle
self.state = State::Idle;
} else {
// If position is not 0.0, redo homing
self.state = State::Homing(HomingState::Initialize);
}
}
}
},
// If state changed we
State::Traversing(traversing_state) => match traversing_state {
TraversingState::GoingOut => {
// If outer limit is reached
if self.position >= self.limit_outer - self.padding {
// Turn around
self.state = State::Traversing(TraversingState::TraversingIn);
}
}
TraversingState::TraversingIn => {
// If inner limit is reached
if self.position <= self.limit_inner + self.padding {
// Turn around
self.state = State::Traversing(TraversingState::TraversingOut);
}
}
TraversingState::TraversingOut => {
// If outer limit is reached
if self.position >= self.limit_outer - self.padding {
// Turn around
self.state = State::Traversing(TraversingState::TraversingIn);
}
}
},
}
// Set the [`did_change_state`] flag
if !self.did_change_state {
self.did_change_state = self.update_did_change_state(&old_state);
}
// Speed
match &self.state {
State::NotHomed => Velocity::ZERO, // Not homed, no movement
State::Idle => Velocity::ZERO, // No movement in idle state
State::GoingIn => {
// Move in at a speed of 10-100 mm/s
self.speed_to_position(
self.limit_inner,
match self.distance_to_position(self.limit_inner).abs()
> Length::new::<millimeter>(1.0)
{
true => Velocity::new::<millimeter_per_second>(100.0),
false => Velocity::new::<millimeter_per_second>(10.0),
},
)
}
State::GoingOut => {
// Move out at a speed of 10-100 mm/s
self.speed_to_position(
self.limit_outer,
match self.distance_to_position(self.limit_outer).abs()
> Length::new::<millimeter>(1.0)
{
true => Velocity::new::<millimeter_per_second>(100.0),
false => Velocity::new::<millimeter_per_second>(10.0),
},
)
}
State::Homing(homing_state) => match homing_state {
HomingState::Initialize => Velocity::ZERO,
HomingState::EscapeEndstop => {
// Move out at a speed of 10 mm/s
Velocity::new::<millimeter_per_second>(10.0)
}
HomingState::FindEndstopFineDistancing => {
// Move out at a speed of 2 mm/s
Velocity::new::<millimeter_per_second>(2.0)
}
HomingState::FindEndstopCoarse => {
// Move in at a speed of -100 mm/s
Velocity::new::<millimeter_per_second>(-100.0)
}
HomingState::FindEndtopFine => {
// move into the endstop at 2 mm/s
Velocity::new::<millimeter_per_second>(-2.0)
}
HomingState::Validate(_) => {
// We stand still until the validation cooldown has passed
Velocity::ZERO
}
}, // Homing speed
State::Traversing(traversing_state) => match traversing_state {
TraversingState::GoingOut => {
// Move out at a speed of 100 mm/s
self.speed_to_position(
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
Velocity::new::<millimeter_per_second>(100.0),
)
}
TraversingState::TraversingIn => self.speed_to_position(
self.limit_inner + self.padding - Length::new::<millimeter>(0.01),
Self::calculate_traverse_speed(spool_speed, self.step_size),
),
TraversingState::TraversingOut => self.speed_to_position(
self.limit_outer - self.padding + Length::new::<millimeter>(0.01),
Self::calculate_traverse_speed(spool_speed, self.step_size),
),
},
}
}
/// Calculate the traverse speed
///
/// The traverse speed is the linear speed at which the winding mechanism moves along the spool.
/// It's directly proportional to how fast the spool rotates and how far the traverse moves per rotation.
///
/// - Traverse Distance per Revolution [mm] = Step Size [mm]
/// - Traverse Speed [mm/s] = Spool Speed [rev/s or rad/s] * Step Size [mm]
///
/// Note: While the traverse range (from outer limit minus padding to inner limit plus padding)
/// determines the total area to be covered, the traverse speed itself depends only on
/// the step size and spool rotation speed.
pub fn calculate_traverse_speed(spool_speed: AngularVelocity, step_size: Length) -> Velocity {
// Calculate the traverse speed directly from spool speed and step size
let traverse_speed: Velocity = Velocity::new::<millimeter_per_second>(
spool_speed.get::<revolution_per_second>() * step_size.get::<millimeter>(),
);
traverse_speed
}
pub fn update_speed(
&mut self,
traverse: &mut StepperVelocityEL70x1,
traverse_end_stop: &DigitalInput,
spool_speed: AngularVelocity,
) {
let speed = self.get_speed(traverse, traverse_end_stop, spool_speed);
let steps_per_second = self.fullstep_converter.velocity_to_steps(speed);
// ignore if we can't set speed
let _ = traverse.set_speed(steps_per_second);
}
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use super::*;
#[test]
fn test_calculate_traverse_speed() {
let spool_speed = AngularVelocity::new::<revolution_per_second>(1.0); // 1 revolution per second
let step_size = Length::new::<millimeter>(1.75); // 1.75 mm step size
let traverse_speed = TraverseController::calculate_traverse_speed(spool_speed, step_size);
assert_relative_eq!(
traverse_speed.get::<millimeter_per_second>(),
1.75,
epsilon = f64::EPSILON
);
}
}
+35
View File
@@ -0,0 +1,35 @@
use ethercat_hal::io::{
analog_input::AnalogInput,
digital_input::DigitalInput,
digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Mode
{
Standby,
Hold,
Pull,
Wind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpoolLengthTaskCompletedAction
{
NoAction,
Pull,
Hold,
}
#[derive(Debug)]
pub struct Hardware
{
pub spool_motor: StepperVelocityEL70x1,
pub traverse_motor: StepperVelocityEL70x1,
pub traverse_limit_switch: DigitalInput,
pub traverse_laser_pointer: DigitalOutput,
pub puller_motor: StepperVelocityEL70x1,
pub tension_arm_sensor: AnalogInput,
}
+54 -1
View File
@@ -3,6 +3,8 @@ use crate::{
socketio::main_namespace::machines_event::MachineObj,
};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use control_core::socketio::event::GenericEvent;
use machines::{
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial,
MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
@@ -13,8 +15,8 @@ use machines::{
},
registry::{MACHINE_REGISTRY, MachineRegistry},
serial::{devices::laser::Laser, init::SerialDetection},
winder2::api::GenericEvent,
};
use socketio::{
main_namespace::{MainNamespaceEvents, ethercat_devices_event::EthercatDevicesEventBuilder},
queue::start_socketio_queue,
@@ -256,6 +258,7 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
while let Ok(message) = recv.recv().await {
match message {
AsyncThreadMessage::NoMsg => (),
AsyncThreadMessage::ConnectOneWayRequest(cross_connection) => {
let api_machines_guard = shared_state.api_machines.lock().await;
// The Src Connection is from the machine that recvs the request to connect
@@ -267,6 +270,8 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
None => continue,
};
tracing::warn!("Received signal for ONE WAY REQUEST");
let dest_sender = match api_machines_guard.get(&dest_ident) {
Some(sender) => sender,
None => continue,
@@ -283,6 +288,54 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
Err(_) => tracing::error!("Failed to send MachineConnection"),
}
}
AsyncThreadMessage::ConnectTwoWayRequest(cross_connection) => {
let api_machines_guard = shared_state.api_machines.lock().await;
let src_ident = cross_connection.src;
let dest_ident = cross_connection.dest;
let src_sender = match api_machines_guard.get(&src_ident) {
Some(sender) => sender,
None => continue,
};
let dest_sender = match api_machines_guard.get(&dest_ident) {
Some(sender) => sender,
None => continue,
};
// connect src -> dest
let connection_src_to_dest = MachineConnection {
ident: dest_ident.clone(),
connection: dest_sender.clone(),
};
let res = src_sender
.send(machines::MachineMessage::ConnectToMachine(
connection_src_to_dest,
))
.await;
match res {
Ok(_) => (),
Err(_) => tracing::error!("Failed to send MachineConnection src -> dest"),
}
// connect dest -> src
let connection_dest_to_src = MachineConnection {
ident: src_ident.clone(),
connection: src_sender.clone(),
};
let res = dest_sender
.send(machines::MachineMessage::ConnectToMachine(
connection_dest_to_src,
))
.await;
match res {
Ok(_) => (),
Err(_) => tracing::error!("Failed to send MachineConnection dest -> src"),
}
}
AsyncThreadMessage::DisconnectMachines(cross_connection) => {
let api_machines_guard = shared_state.api_machines.lock().await;
// The Src Connection is from the machine that recvs the request to connect