diff --git a/electron/src/machines/winder/gluetex/GluetexControlPage.tsx b/electron/src/machines/winder/gluetex/GluetexControlPage.tsx new file mode 100644 index 00000000..cab121be --- /dev/null +++ b/electron/src/machines/winder/gluetex/GluetexControlPage.tsx @@ -0,0 +1,376 @@ +import { ControlCard } from "@/control/ControlCard"; +import { Page } from "@/components/Page"; +import React, { useState } from "react"; +import { ControlGrid } from "@/control/ControlGrid"; +import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue"; +import { TraverseBar } from "../TraverseBar"; +import { + SelectionGroup, + SelectionGroupBoolean, +} from "@/control/SelectionGroup"; +import { EditValue } from "@/control/EditValue"; +import { Label } from "@/control/Label"; +import { TouchButton } from "@/components/touch/TouchButton"; +import { StatusBadge } from "@/control/StatusBadge"; +import { useGluetex } from "./useGluetex"; +import { + Mode, + SpoolAutomaticActionMode, + getGearRatioMultiplier, +} from "./gluetexNamespace"; +import { TensionArm } from "../TensionArm"; +import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal"; +import { Spool } from "../Spool"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { getGluetexTraverseMax } from "./gluetexConfig"; + +export function GluetexControlPage() { + const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false); + const traverseMax = getGluetexTraverseMax(); + + // use optimistic state + const { + state, + defaultState, + enableTraverseLaserpointer, + tensionArmAngle, + zeroTensionArmAngle, + spoolRpm, + setMode, + pullerSpeed, + spoolProgress, + setPullerRegulationMode, + setPullerTargetSpeed, + traversePosition, + resetSpoolProgress, + setTraverseLimitInner, + setTraverseLimitOuter, + gotoTraverseLimitInner, + gotoTraverseLimitOuter, + gotoTraverseHome, + setSpoolAutomaticRequiredMeters, + setSpoolAutomaticAction, + isLoading, + isDisabled, + } = useGluetex(); + + // 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 handleResetProgress = () => { + // Check if the machine is currently in Wind mode + if (state?.mode_state?.mode === "Wind") { + setShowResetConfirmDialog(true); + } else { + resetSpoolProgress(); + } + }; + + const confirmResetProgress = () => { + resetSpoolProgress(); + setShowResetConfirmDialog(false); + }; + + return ( + + + + + roundToDecimals(value, 0)} + /> + + + + roundToDecimals(value, 1)} + /> + {state?.traverse_state && ( + + )} +
+ + +
+ + +
+ + + + roundDegreesToDecimals(value, 0)} + /> + + Set Zero Point + + {!state?.tension_arm_state?.zeroed && ( + Not Zeroed + )} + + + + + value={state?.mode_state.mode} + disabled={isDisabled} + loading={isLoading} + onChange={setMode} + orientation="vertical" + className="grid h-full grid-cols-2 gap-2" + options={{ + Standby: { + children: "Standby", + icon: "lu:Power", + isActiveClassName: "bg-green-600", + className: "h-full", + }, + Hold: { + children: "Hold", + icon: "lu:CirclePause", + isActiveClassName: "bg-green-600", + className: "h-full", + }, + Pull: { + children: "Pull", + icon: "lu:ChevronsLeft", + isActiveClassName: "bg-green-600", + className: "h-full", + }, + Wind: { + children: "Wind", + icon: "lu:RefreshCcw", + isActiveClassName: "bg-green-600", + disabled: !state?.mode_state?.can_wind, + className: "h-full", + }, + }} + /> + + + + roundToDecimals(value, 1)} + /> + + + + + roundToDecimals(value, 2)} + unit="m" + timeseries={spoolProgress} + /> + + + + + Reset Progress + + + + +
+ + {/* Reset Progress Confirmation Dialog */} + + + + Reset Spool Progress? + + The machine is currently in Wind mode. Are you sure you want to + reset the spool progress? + + + +
+ + Yes, Reset Progress + + setShowResetConfirmDialog(false)} + > + Cancel + +
+
+
+
+ ); +} diff --git a/electron/src/machines/winder/gluetex/GluetexGraphs.tsx b/electron/src/machines/winder/gluetex/GluetexGraphs.tsx new file mode 100644 index 00000000..1edd9107 --- /dev/null +++ b/electron/src/machines/winder/gluetex/GluetexGraphs.tsx @@ -0,0 +1,290 @@ +import { Page } from "@/components/Page"; +import { + AutoSyncedBigGraph, + SyncedFloatingControlPanel, + useGraphSync, + type GraphConfig, + type GraphLine, +} from "@/components/graph"; +import React from "react"; +import { useGluetex } from "./useGluetex"; +import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal"; +import { TimeSeries } from "@/lib/timeseries"; +import { Unit } from "@/control/units"; + +export function GluetexGraphsPage() { + const { + state, + spoolRpm, + traversePosition, + tensionArmAngle, + pullerSpeed, + spoolProgress, + } = useGluetex(); + + const syncHook = useGraphSync("gluetex-group"); + + return ( + +
+
+ roundToDecimals(value, 0)} + /> + + roundDegreesToDecimals(value, 0)} + /> + + roundToDecimals(value, 0)} + /> + + roundToDecimals(value, 1)} + /> + + roundToDecimals(value, 2)} + /> +
+
+ + +
+ ); +} + +export function SpoolRpmGraph({ + syncHook, + newData, + unit, + renderValue, +}: { + syncHook: ReturnType; + newData: TimeSeries | null; + unit?: Unit; + renderValue?: (value: number) => string; +}) { + const config: GraphConfig = { + title: "Spool Speed", + icon: "lu:RotateCcw", + colors: { + primary: "#10b981", + grid: "#e2e8f0", + background: "#ffffff", + }, + exportFilename: "spool_rpm_data", + }; + + return ( + + ); +} + +export function TraversePositionGraph({ + syncHook, + newData, + limitInner, + limitOuter, + unit, + renderValue, +}: { + syncHook: ReturnType; + newData: TimeSeries | null; + limitInner?: number; + limitOuter?: number; + unit?: Unit; + renderValue?: (value: number) => string; +}) { + const lines: GraphLine[] = []; + + if (limitInner !== undefined) { + lines.push({ + type: "threshold", + value: limitInner, + color: "#8b5cf6", + dash: [5, 5], + }); + } + + if (limitOuter !== undefined) { + lines.push({ + type: "threshold", + value: limitOuter, + color: "#8b5cf6", + dash: [5, 5], + }); + } + + const config: GraphConfig = { + title: "Traverse Position", + icon: "lu:Move", + colors: { + primary: "#8b5cf6", + }, + exportFilename: "traverse_position_data", + }; + + return ( + + ); +} + +export function TensionArmAngleGraph({ + syncHook, + newData, + unit, + renderValue, +}: { + syncHook: ReturnType; + newData: TimeSeries | null; + unit?: Unit; + renderValue?: (value: number) => string; +}) { + const config: GraphConfig = { + title: "Tension Arm Angle", + icon: "lu:RotateCw", + colors: { + primary: "#f59e0b", + }, + exportFilename: "tension_arm_angle_data", + }; + + return ( + + ); +} + +export function SpoolProgressGraph({ + syncHook, + newData, + unit, + renderValue, +}: { + syncHook: ReturnType; + newData: TimeSeries | null; + unit?: Unit; + renderValue?: (value: number) => string; +}) { + const config: GraphConfig = { + title: "Spool Progress", + icon: "lu:RotateCw", + colors: { + primary: "#f59e0b", + }, + exportFilename: "spool_progress", + }; + + return ( + + ); +} + +export function PullerSpeedGraph({ + syncHook, + newData, + targetSpeed, + unit, + renderValue, +}: { + syncHook: ReturnType; + newData: TimeSeries | null; + targetSpeed?: number; + unit?: Unit; + renderValue?: (value: number) => string; +}) { + const lines: GraphLine[] = []; + + if (targetSpeed !== undefined) { + lines.push({ + type: "target", + value: targetSpeed, + label: "Target Speed", + color: "#06b6d4", // Match series color + }); + } + + const config: GraphConfig = { + title: "Puller Speed", + icon: "lu:Gauge", + colors: { + primary: "#06b6d4", + }, + exportFilename: "puller_speed_data", + }; + + return ( + + ); +} diff --git a/electron/src/machines/winder/gluetex/GluetexManual.tsx b/electron/src/machines/winder/gluetex/GluetexManual.tsx new file mode 100644 index 00000000..9a6cb9f1 --- /dev/null +++ b/electron/src/machines/winder/gluetex/GluetexManual.tsx @@ -0,0 +1,12 @@ +import { Page } from "@/components/Page"; +import { MarkdownWithToc } from "@/components/MarkdownWithToc"; +import React from "react"; +import winderManualContent from "@/assets/markdown/winder/manual.md?raw"; + +export function GluetexManualPage() { + return ( + + + + ); +} diff --git a/electron/src/machines/winder/gluetex/GluetexPage.tsx b/electron/src/machines/winder/gluetex/GluetexPage.tsx new file mode 100644 index 00000000..9bec6667 --- /dev/null +++ b/electron/src/machines/winder/gluetex/GluetexPage.tsx @@ -0,0 +1,42 @@ +import { Topbar } from "@/components/Topbar"; +import React from "react"; + +export function GluetexPage() { + return ( + + ); +} diff --git a/electron/src/machines/winder/gluetex/GluetexPresetsPage.tsx b/electron/src/machines/winder/gluetex/GluetexPresetsPage.tsx new file mode 100644 index 00000000..51f4e401 --- /dev/null +++ b/electron/src/machines/winder/gluetex/GluetexPresetsPage.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Page } from "@/components/Page"; + +export function GluetexPresetsPage() { + return ( + +
+

Gluetex Presets

+

+ Preset functionality is not available in this test version. This is a + hardcoded frontend for testing purposes only. +

+
+
+ ); +} diff --git a/electron/src/machines/winder/gluetex/GluetexSettings.tsx b/electron/src/machines/winder/gluetex/GluetexSettings.tsx new file mode 100644 index 00000000..dd2734a8 --- /dev/null +++ b/electron/src/machines/winder/gluetex/GluetexSettings.tsx @@ -0,0 +1,374 @@ +import { Page } from "@/components/Page"; +import { ControlCard } from "@/control/ControlCard"; +import { ControlGrid } from "@/control/ControlGrid"; +import { EditValue } from "@/control/EditValue"; +import React, { useState } from "react"; +import { useGluetex } from "./useGluetex"; +import { roundToDecimals } from "@/lib/decimal"; +import { Label } from "@/control/Label"; +import { SelectionGroupBoolean } from "@/control/SelectionGroup"; +import { SelectionGroup } from "@/control/SelectionGroup"; +import { MachineSelector } from "@/components/MachineConnectionDropdown"; +import { + getGluetexXLMode, + setGluetexXLMode, + GLUETEX_TRAVERSE_MAX_STANDARD, + GLUETEX_TRAVERSE_MAX_XL, +} from "./gluetexConfig"; + +export function GluetexSettingPage() { + const [xlMode, setXlMode] = useState(getGluetexXLMode()); + + const { + state, + defaultState, + setTraverseStepSize, + setTraversePadding, + setTraverseLimitInner, + setTraverseLimitOuter, + gotoTraverseHome, + setPullerForward, + setPullerGearRatio, + setSpoolRegulationMode, + setSpoolMinMaxMinSpeed, + setSpoolMinMaxMaxSpeed, + setSpoolForward, + setSpoolAdaptiveTensionTarget, + setSpoolAdaptiveRadiusLearningRate, + setSpoolAdaptiveMaxSpeedMultiplier, + setSpoolAdaptiveAccelerationFactor, + setSpoolAdaptiveDeaccelerationUrgencyMultiplier, + isLoading, + isDisabled, + selectedMachine, + filteredMachines, + setConnectedMachine, + disconnectMachine, + } = useGluetex(); + + const handleXlModeChange = (enabled: boolean) => { + setGluetexXLMode(enabled); + setXlMode(enabled); + + // When switching from XL to normal mode, reset traverse limits to default values + if (!enabled && defaultState) { + // Only reset if current values exceed the standard max + const currentOuter = state?.traverse_state?.limit_outer ?? 0; + const currentInner = state?.traverse_state?.limit_inner ?? 0; + const defaultOuter = defaultState.traverse_state?.limit_outer; + const defaultInner = defaultState.traverse_state?.limit_inner; + + if ( + currentOuter > GLUETEX_TRAVERSE_MAX_STANDARD && + defaultOuter !== undefined + ) { + setTraverseLimitOuter(defaultOuter); + setTraverseLimitInner(defaultInner); + } + if ( + currentInner > GLUETEX_TRAVERSE_MAX_STANDARD && + defaultInner !== undefined + ) { + setTraverseLimitOuter(defaultOuter); + setTraverseLimitInner(defaultInner); + } + + // Home the traverse when switching from XL to normal mode + gotoTraverseHome(); + } + }; + + return ( + + + + + + + + + + + + + + {state?.spool_speed_controller_state?.regulation_mode === + "MinMax" && ( + <> + + + + )} + + {state?.spool_speed_controller_state?.regulation_mode === + "Adaptive" && ( +
+ + + +