Enforce LinuxCNC WASM authority for XYZAC frontend
This commit is contained in:
@@ -8,7 +8,7 @@ export const AXIS_COMMANDS = Object.freeze({
|
||||
open: "LOAD_WORKSPACE", load: "PROGRAM_LOAD",
|
||||
run: "PROGRAM_RUN", step: "PROGRAM_STEP", pause: "PROGRAM_PAUSE", resume: "PROGRAM_RESUME",
|
||||
stop: "PROGRAM_ABORT", restart: "PROGRAM_RESTART", mdi: "MDI_EXECUTE", jogStart: "JOG_START",
|
||||
jogStop: "JOG_STOP", home: "HOME", override: "SET_OVERRIDE", estop: "ESTOP", recover: "IO_RECOVER",
|
||||
jogStop: "JOG_STOP", jogScale: "SET_JOG_SCALE", maxVelocity: "SET_MAX_VELOCITY", spindleOverride: "SET_SPINDLE_OVERRIDE", home: "HOME", override: "SET_OVERRIDE", estop: "ESTOP", recover: "IO_RECOVER",
|
||||
power: "POWER",
|
||||
spindle: "SPINDLE_COMMAND", coolant: "COOLANT_COMMAND", toolPrepare: "TOOL_PREPARE",
|
||||
toolChange: "TOOL_CHANGE", probe: "PROBE_CONFIG",
|
||||
|
||||
@@ -40,9 +40,9 @@ function AxisRadio({ axis, selected, onChange }: { axis: string; selected: strin
|
||||
return <label className={`axis-radio${selected === axis ? " selected" : ""}`}><input type="radio" name="axis" value={axis} checked={selected === axis} onChange={() => onChange(axis)} /><span>{axis.toUpperCase()}</span></label>;
|
||||
}
|
||||
|
||||
function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programRunning, programLoading, canPower, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { isEstop: boolean; machineOn: boolean; allHomed: boolean; programLoaded: boolean; programRunning: boolean; programLoading: boolean; canPower: boolean; canHome: boolean; canRun: boolean; onEstop: () => void; onRelease: () => void; onPower: () => void; onHomeAll: () => void; onRun: () => void; onSequence: () => void }) {
|
||||
function MachineSequence({ wasmReady, isEstop, machineOn, allHomed, programLoaded, programRunning, programLoading, canPower, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { wasmReady: boolean; isEstop: boolean; machineOn: boolean; allHomed: boolean; programLoaded: boolean; programRunning: boolean; programLoading: boolean; canPower: boolean; canHome: boolean; canRun: boolean; onEstop: () => void; onRelease: () => void; onPower: () => void; onHomeAll: () => void; onRun: () => void; onSequence: () => void }) {
|
||||
return <section className="machine-sequence" data-axis-feature="AXIS-MACHINE-SEQUENCE" aria-label="Machine sequence">
|
||||
<div className="machine-sequence-heading"><strong>Machine sequence</strong><span>{isEstop ? "ESTOP" : machineOn ? "ON" : "OFF"} / {allHomed ? "HOME ALL" : "NOT HOMED"}</span></div>
|
||||
<div className="machine-sequence-heading"><strong>Machine sequence</strong><span>{wasmReady ? (isEstop ? "ESTOP" : machineOn ? "ON" : "OFF") : "WASM PENDING"} / {wasmReady ? (allHomed ? "HOME ALL" : "NOT HOMED") : "WASM PENDING"}</span></div>
|
||||
<div className="machine-sequence-actions" role="group" aria-label="Emergency stop, power, homing and program controls">
|
||||
<TextButton danger disabled={isEstop} onClick={onEstop} ariaLabel="Emergency stop">Emergency Stop</TextButton>
|
||||
<TextButton disabled={!isEstop} onClick={onRelease} ariaLabel="Release emergency stop">Release E-stop</TextButton>
|
||||
@@ -54,21 +54,17 @@ function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programR
|
||||
</section>;
|
||||
}
|
||||
|
||||
function ManualControl({ action, ready, canHome, onHomeAll, selectedAxis, setSelectedAxis, spindleOn, coolant, onSpindle, onCoolant, devices }: { action: Action; ready: boolean; canHome: boolean; onHomeAll: () => void; selectedAxis: string; setSelectedAxis: (axis: string) => void; spindleOn: boolean; coolant: { mist: boolean; flood: boolean }; onSpindle: (direction: number) => void; onCoolant: (key: "mist" | "flood") => void; devices: Record<string, any> }) {
|
||||
function ManualControl({ action, canJog, canHome, canSpindle, canCoolant, canTool, canProbe, canTouchOff, canToolTouchOff, onHomeAll, selectedAxis, setSelectedAxis, spindleOn, spindleBrake, coolant, onSpindle, onCoolant, jogScale, onJogScale, devices }: { action: Action; canJog: boolean; canHome: boolean; canSpindle: boolean; canCoolant: boolean; canTool: boolean; canProbe: boolean; canTouchOff: boolean; canToolTouchOff: boolean; onHomeAll: () => void; selectedAxis: string; setSelectedAxis: (axis: string) => void; spindleOn: boolean; spindleBrake: boolean; coolant: { mist: boolean; flood: boolean }; onSpindle: (direction: number) => void; onCoolant: (key: "mist" | "flood") => void; jogScale: number | null; onJogScale: (event: React.ChangeEvent<HTMLInputElement>) => void; devices: Record<string, any> }) {
|
||||
const [increment, setIncrement] = useState("Continuous");
|
||||
const [jogSpeed, setJogSpeed] = useState(50);
|
||||
const [maxVelocity, setMaxVelocity] = useState(100);
|
||||
const [overrideLimits, setOverrideLimits] = useState(false);
|
||||
const [brake, setBrake] = useState(false);
|
||||
const [jogging, setJogging] = useState(false);
|
||||
const jog = (direction: number) => { setJogging(true); const request = action("JOG_START", { axis: selectedAxis, direction, speed: increment === "Continuous" ? 1 : 1, increment: increment === "Continuous" ? null : Number(increment) }); if (increment !== "Continuous" && request && typeof (request as Promise<void>).then === "function") void (request as Promise<void>).then(() => action("SIM_STEP", { cycles: 1 })); };
|
||||
const jog = (direction: number) => { setJogging(true); const request = action("JOG_START", { axis: selectedAxis, direction, increment: increment === "Continuous" ? null : Number(increment) }); if (increment !== "Continuous" && request && typeof (request as Promise<void>).then === "function") void (request as Promise<void>).then(() => action("SIM_STEP", { cycles: 1 })); };
|
||||
const stopJog = () => { if (!jogging) return; setJogging(false); void action("JOG_STOP"); };
|
||||
return <section className="manual-control" data-axis-feature="AXIS-MANUAL-01" aria-label="Manual Control"><div className="manual-grid">
|
||||
<div className="manual-axis-block"><h3>Axis:</h3><div className="axis-radio-grid">{["x", "y", "z", "a", "c"].map((axis) => <AxisRadio key={axis} axis={axis} selected={selectedAxis} onChange={setSelectedAxis} />)}</div></div>
|
||||
<div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!ready} onPointerDown={() => jog(-1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} negative`}>-</TextButton><TextButton disabled={!ready} onPointerDown={() => jog(1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} positive`}>+</TextButton><select value={increment} onChange={(event) => setIncrement(event.target.value)} aria-label="Jog increment"><option>Continuous</option><option value="0.1000">0.1000</option><option value="0.0100">0.0100</option><option value="0.0010">0.0010</option><option value="0.0001">0.0001</option></select><TextButton disabled={!ready} onClick={stopJog}>Stop</TextButton></div><div className="manual-action-row"><TextButton disabled={!canHome} onClick={() => action("HOME", { axis: selectedAxis })}>Home Axis</TextButton><TextButton disabled={!canHome} onClick={onHomeAll}>Home All</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" checked={overrideLimits} onChange={(event) => setOverrideLimits(event.target.checked)} /> Override Limits</label></div>
|
||||
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!ready} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!ready} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!ready} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={brake} onChange={(event) => { const engaged = event.target.checked; setBrake(engaged); void action("SPINDLE_BRAKE", { spindle: 0, engaged }); }} /> Brake</label></div></div>
|
||||
<div className="manual-coolant-block"><h3>Coolant:</h3><label className="check-line"><input type="checkbox" checked={coolant.mist} onChange={() => onCoolant("mist")} /> Mist</label><label className="check-line"><input type="checkbox" checked={coolant.flood} onChange={() => onCoolant("flood")} /> Flood</label></div>
|
||||
</div><div className="manual-device-block"><h3>Tool / Probe:</h3><div className="manual-action-row"><TextButton disabled={!ready} onClick={() => action("TOOL_PREPARE", { tool: 1, pocket: 1 })}>Prepare T1</TextButton><TextButton disabled={!ready} onClick={() => action("TOOL_CHANGE", {})}>Change</TextButton><TextButton disabled={!ready} onClick={() => action("PROBE_CONFIG", { action: "arm", probe_type: 0 })}>Arm Probe</TextButton><TextButton disabled={!ready} onClick={() => action("PROBE_CONFIG", { action: "disarm" })}>Disarm</TextButton></div><span className="device-status">Tool {devices.tool?.current ?? 0} / {devices.tool?.state ?? "idle"}; Probe {devices.probe?.state ?? "idle"}</span></div><div className="manual-sliders"><label>Jog Speed:<output>{jogSpeed}%</output><input type="range" min="6" max="100" value={jogSpeed} onChange={(event) => setJogSpeed(Number(event.target.value))} /></label><label>Max Velocity:<output>{maxVelocity}%</output><input type="range" min="6" max="100" value={maxVelocity} onChange={(event) => setMaxVelocity(Number(event.target.value))} /></label></div></section>;
|
||||
<div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!canJog} onPointerDown={() => jog(-1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} negative`}>-</TextButton><TextButton disabled={!canJog} onPointerDown={() => jog(1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} positive`}>+</TextButton><select value={increment} onChange={(event) => setIncrement(event.target.value)} aria-label="Jog increment"><option>Continuous</option><option value="0.1000">0.1000</option><option value="0.0100">0.0100</option><option value="0.0010">0.0010</option><option value="0.0001">0.0001</option></select><TextButton disabled={!canJog} onClick={stopJog}>Stop</TextButton></div><div className="manual-action-row"><TextButton disabled={!canHome} onClick={() => action("HOME", { axis: selectedAxis })}>Home Axis</TextButton><TextButton disabled={!canHome} onClick={onHomeAll}>Home All</TextButton><TextButton disabled={!canTouchOff} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!canToolTouchOff} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" disabled title="LinuxCNC WASM override-limits command unavailable" /> Override Limits (WASM unavailable)</label></div>
|
||||
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!canSpindle} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!canSpindle} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!canSpindle} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!canSpindle} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!canSpindle} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={spindleBrake} disabled={!canSpindle} onChange={(event) => void action("SPINDLE_BRAKE", { spindle: 0, engaged: event.target.checked })} /> Brake</label></div></div>
|
||||
<div className="manual-coolant-block"><h3>Coolant:</h3><label className="check-line"><input type="checkbox" checked={coolant.mist} disabled={!canCoolant} onChange={() => onCoolant("mist")} /> Mist</label><label className="check-line"><input type="checkbox" checked={coolant.flood} disabled={!canCoolant} onChange={() => onCoolant("flood")} /> Flood</label></div>
|
||||
</div><div className="manual-device-block"><h3>Tool / Probe:</h3><div className="manual-action-row"><TextButton disabled={!canTool} onClick={() => action("TOOL_PREPARE", { tool: 1, pocket: 1 })}>Prepare T1</TextButton><TextButton disabled={!canTool} onClick={() => action("TOOL_CHANGE", {})}>Change</TextButton><TextButton disabled={!canProbe} onClick={() => action("PROBE_CONFIG", { action: "arm", probe_type: 0 })}>Arm Probe</TextButton><TextButton disabled={!canProbe} onClick={() => action("PROBE_CONFIG", { action: "disarm" })}>Disarm</TextButton></div><span className="device-status">{devices.tool ? `Tool ${devices.tool.current ?? 0} / ${devices.tool.state ?? "idle"}; Probe ${devices.probe?.state ?? "idle"}` : "WASM state pending"}</span></div><div className="manual-sliders"><label>Jog Speed:<output>{jogScale === null ? "-" : `${Math.round(jogScale * 100)}%`}</output><input type="range" min="6" max="100" value={jogScale === null ? 6 : Math.round(jogScale * 100)} onChange={onJogScale} disabled={!canJog || jogScale === null} /></label></div></section>;
|
||||
}
|
||||
|
||||
function MdiControl({ action, ready }: { action: Action; ready: boolean }) {
|
||||
@@ -81,12 +77,13 @@ function MdiControl({ action, ready }: { action: Action; ready: boolean }) {
|
||||
function DroView({ axes, state, coordType, displayType, metric, setCoordType, setDisplayType, setMetric }: { axes: number[]; state: typeof INITIAL_STATE; coordType: "machine" | "relative"; displayType: "actual" | "commanded"; metric: boolean; setCoordType: (value: "machine" | "relative") => void; setDisplayType: (value: "actual" | "commanded") => void; setMetric: (value: boolean) => void }) {
|
||||
const labels = ["X", "Y", "Z", "A", "C"];
|
||||
const scale = metric ? 1 : 1 / 25.4;
|
||||
const format = (value: number, index: number) => (value * (index < 3 ? scale : 1)).toFixed(4);
|
||||
const format = (value: number, index: number) => Number.isFinite(value) ? (value * (index < 3 ? scale : 1)).toFixed(4) : "-";
|
||||
const sourceReady = state.snapshot?.motion_authority === "wasm" && Boolean(state.snapshot?.task_authority);
|
||||
const pose = (state.snapshot?.visual?.pose ?? state.snapshot?.authoritative?.kinematics?.pose) as Record<string, number> | undefined;
|
||||
const commanded = [pose?.x, pose?.y, pose?.z, pose?.a, pose?.c].map((value, index) => Number.isFinite(value) ? Number(value) : Number(axes[index] ?? 0));
|
||||
const commanded = [pose?.x, pose?.y, pose?.z, pose?.a, pose?.c].map((value, index) => sourceReady && Number.isFinite(value) ? Number(value) : sourceReady ? Number(axes[index] ?? Number.NaN) : Number.NaN);
|
||||
const actualPose = (state.snapshot?.visual?.pose ?? state.snapshot?.kinematics?.pose ?? {}) as Record<string, number>;
|
||||
const actualAxes = [actualPose.x, actualPose.y, actualPose.z, actualPose.a, actualPose.c].map((value, index) => Number.isFinite(value) ? Number(value) : Number(axes[index] ?? 0));
|
||||
const relative = state.snapshot?.coordinates?.relative ?? [0, 0, 0, 0, 0];
|
||||
const actualAxes = [actualPose.x, actualPose.y, actualPose.z, actualPose.a, actualPose.c].map((value, index) => sourceReady && Number.isFinite(value) ? Number(value) : sourceReady ? Number(axes[index] ?? Number.NaN) : Number.NaN);
|
||||
const relative = sourceReady ? state.snapshot?.coordinates?.relative ?? [Number.NaN, Number.NaN, Number.NaN, Number.NaN, Number.NaN] : [Number.NaN, Number.NaN, Number.NaN, Number.NaN, Number.NaN];
|
||||
return <section className="dro-view" data-axis-feature="AXIS-VIEW-04" role="tabpanel" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><label>Coordinate<select data-axis-feature="AXIS-VIEW-18" value={coordType} onChange={(event) => setCoordType(event.target.value as "machine" | "relative")}><option value="machine">Machine</option><option value="relative">Relative</option></select></label><label>Display<select data-axis-feature="AXIS-VIEW-17" value={displayType} onChange={(event) => setDisplayType(event.target.value as "actual" | "commanded")}><option value="actual">Actual</option><option value="commanded">Commanded</option></select></label><label>Units<select data-axis-feature="AXIS-VIEW-02" value={metric ? "mm" : "inch"} onChange={(event) => setMetric(event.target.value === "mm")}><option value="mm">mm</option><option value="inch">inch</option></select></label></div><div className="dro-column-headings"><span /><span>Displayed</span><span>Actual</span><span>Relative</span></div><div className="dro-table">{labels.map((label, index) => { const actual = actualAxes[index]; const target = Number(commanded[index] ?? actual); const relativeValue = Number(relative[index] ?? actual); const displayed = displayType === "commanded" ? target : actual; return <div className="dro-row" key={label}><strong>{label}</strong><output>{format(coordType === "relative" ? relativeValue : displayed, index)}</output><output>{format(actual, index)}</output><output>{format(relativeValue, index)}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>{metric ? "mm" : "inch"}</b> / deg</span><span>{coordType} / {displayType}</span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
|
||||
}
|
||||
|
||||
@@ -176,8 +173,6 @@ export function App() {
|
||||
const [view, setView] = useState<"z" | "z2" | "x" | "y" | "y2" | "p">("p");
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [rotateMode, setRotateMode] = useState(false);
|
||||
const [blockDelete, setBlockDelete] = useState(false);
|
||||
const [optionalStop, setOptionalStop] = useState(false);
|
||||
const [coordType, setCoordType] = useState<"machine" | "relative">("machine");
|
||||
const [displayType, setDisplayType] = useState<"actual" | "commanded">("actual");
|
||||
const [metric, setMetric] = useState(true);
|
||||
@@ -189,30 +184,31 @@ export function App() {
|
||||
const programFileInput = useRef<HTMLInputElement>(null);
|
||||
const [plotCleared, setPlotCleared] = useState(false);
|
||||
const [operationTrace, setOperationTrace] = useState<XyzacOperationTrace[]>([]);
|
||||
const [feedOverrideDraft, setFeedOverrideDraft] = useState(100);
|
||||
const feedOverridePending = useRef<number | null>(null);
|
||||
const feedOverrideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [previewProgress, setPreviewProgress] = useState<{ percent: number; phase: "reading" | "wasm" | "ready" | "error"; detail: string } | null>(null);
|
||||
const previewProgressTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().then(async () => { const bundle = await loadSimulationCase({ path: DEFAULT_CASE, baseUrl: "." }); if (bundle.status !== "ready") throw new Error(`xyzac_case_blocked:${bundle.missing.join(",")}`); const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); const normalizedSource = normalizeProgramSource(source); setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: {}, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }); return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); client.close(); }; }, [client, operationController]);
|
||||
const axes = state.snapshot?.motion?.axes ?? [0, 0, 0];
|
||||
const axes = state.snapshot?.motion?.axes ?? [];
|
||||
const taskRecord = state.task ?? {};
|
||||
const axisState = { task: { ...taskRecord, state: taskRecord.state ?? "OFF", mode: taskRecord.mode ?? "MANUAL", interp_state: taskRecord.interp_state ?? (state.status === "running" ? "READING" : state.status === "paused" ? "PAUSED" : "IDLE") }, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
||||
const axisState = { task: taskRecord, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
||||
const xyzacView = useMemo(() => projectXyzacPanel(state.snapshot), [state.snapshot]);
|
||||
const machineState = state.axisState?.machine_state ?? (String(taskRecord.state ?? "OFF").toUpperCase() === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(String(taskRecord.state ?? "").toUpperCase()) ? "ON" : "OFF");
|
||||
const operationState = state.axisState?.operation_state ?? (state.status === "running" ? "RUNNING" : state.status === "paused" ? "PAUSED" : "IDLE");
|
||||
const taskState = state.axisState?.state ?? (machineState === "ESTOP" ? "ESTOP" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "ON" ? "IDLE" : "OFF");
|
||||
const isEstop = machineState === "ESTOP";
|
||||
const machineOn = machineState === "ON";
|
||||
const ready = machineOn && !["error", "recovering"].includes(state.status);
|
||||
// Runtime state is displayed only after a Worker panel snapshot is
|
||||
// available. React may render before WASM boot, but it must not invent a
|
||||
// machine/task/operation state that can enable motion.
|
||||
const wasmSnapshotReady = Boolean(state.snapshot?.motion_authority === "wasm" && state.snapshot?.task_authority);
|
||||
const machineState = wasmSnapshotReady ? state.axisState?.machine_state ?? "OFF" : "PENDING";
|
||||
const operationState = wasmSnapshotReady ? state.axisState?.operation_state ?? "IDLE" : "PENDING";
|
||||
const taskState = wasmSnapshotReady ? state.axisState?.state ?? "OFF" : "PENDING";
|
||||
const isEstop = wasmSnapshotReady && machineState === "ESTOP";
|
||||
const machineOn = wasmSnapshotReady && machineState === "ON";
|
||||
const ready = wasmSnapshotReady && machineOn && !["error", "recovering"].includes(state.status);
|
||||
const programLoaded = Boolean(state.program?.source_hash || state.program?.path || state.program?.loaded || state.program?.program_status && state.program.program_status !== "idle");
|
||||
const programRunning = operationState === "RUNNING";
|
||||
const programPaused = operationState === "PAUSED";
|
||||
const idle = operationState === "IDLE";
|
||||
const wasmGate = (action: string) => state.gates[action] ?? { action, enabled: false, reason: "linuxcnc_wasm_snapshot_pending" };
|
||||
const gates = { load: wasmGate("load"), run: wasmGate("run"), step: wasmGate("step"), pause: wasmGate("pause"), resume: wasmGate("resume"), abort: wasmGate("abort"), mdi: wasmGate("mdi"), jog: wasmGate("jog"), home: wasmGate("home"), power: wasmGate("power"), reset: wasmGate("reset"), override: wasmGate("override") };
|
||||
const planner = state.snapshot?.authoritative?.planner;
|
||||
const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])];
|
||||
const gates = { load: wasmGate("load"), run: wasmGate("run"), step: wasmGate("step"), pause: wasmGate("pause"), resume: wasmGate("resume"), abort: wasmGate("abort"), mdi: wasmGate("mdi"), jog: wasmGate("jog"), home: wasmGate("home"), power: wasmGate("power"), reset: wasmGate("reset"), override: wasmGate("override"), spindle: wasmGate("spindle"), coolant: wasmGate("coolant"), tool: wasmGate("tool"), probe: wasmGate("probe"), touchOff: wasmGate("touchOff"), toolTouchOff: wasmGate("toolTouchOff") };
|
||||
const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } };
|
||||
const stopJogTimer = () => { if (jogTimer.current) { clearInterval(jogTimer.current); jogTimer.current = null; } };
|
||||
const startJogTimer = () => { if (jogTimer.current) return; jogTimer.current = setInterval(() => { if (jogStepPending.current) return; jogStepPending.current = true; operationController.execute("SIM_STEP", { cycles: 1, response: "motion_delta" }, { state: axisState, gate: false }).catch(() => undefined).finally(() => { jogStepPending.current = false; }); }, 50); };
|
||||
@@ -426,39 +422,36 @@ export function App() {
|
||||
if (programRunning && !programComplete) startRunTimer(); else stopRunTimer();
|
||||
if (!ready) { stopRunTimer(); stopJogTimer(); }
|
||||
}, [programRunning, ready, state.program?.program_status, state.snapshot?.wasm_motion]);
|
||||
const spindleOn = Boolean((state.devices as Record<string, any> | null)?.spindles?.[0]?.enabled);
|
||||
const spindleState = (state.devices as Record<string, any> | null)?.spindles?.[0] as Record<string, any> | undefined;
|
||||
const spindleOn = spindleState ? Boolean(spindleState.enabled) : false;
|
||||
const spindleBrake = spindleState ? Boolean(spindleState.brake) : false;
|
||||
const ioState = (state.snapshot?.io as Record<string, any> | undefined)?.state as Record<string, any> | undefined;
|
||||
const manualReady = wasmSnapshotReady && Boolean(ioState?.ready) && !Boolean(ioState?.estop);
|
||||
const coolant = { mist: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.mist_requested), flood: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.flood_requested) };
|
||||
const onSpindle = (direction: number) => { execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 600, enabled: direction !== 0 }); };
|
||||
const onCoolant = (key: "mist" | "flood") => { const next = { ...coolant, [key]: !coolant[key] }; execute("COOLANT_COMMAND", { mist: next.mist, flood: next.flood, lube: false }); };
|
||||
const wasmFeedOverride = Number(state.snapshot?.overrides?.feed ?? 1);
|
||||
const feedOverridePercent = Number.isFinite(wasmFeedOverride) ? Math.max(0, Math.min(200, Math.round(wasmFeedOverride * 100))) : 100;
|
||||
useEffect(() => {
|
||||
if (feedOverridePending.current === null) setFeedOverrideDraft(feedOverridePercent);
|
||||
else if (feedOverridePercent === feedOverridePending.current) {
|
||||
feedOverridePending.current = null;
|
||||
setFeedOverrideDraft(feedOverridePercent);
|
||||
}
|
||||
}, [feedOverridePercent]);
|
||||
const wasmMotion = (state.snapshot?.wasm_motion as { state?: Record<string, unknown> } | undefined)?.state ?? {};
|
||||
const sourceFeedOverride = Number(state.snapshot?.overrides?.feed ?? wasmMotion.feed_override);
|
||||
const feedOverridePercent = Number.isFinite(sourceFeedOverride) ? Math.max(0, Math.min(200, Math.round(sourceFeedOverride * 100))) : null;
|
||||
const sourceJogScale = Number((state.snapshot?.motion as Record<string, unknown> | undefined)?.jog_scale ?? wasmMotion.jog_scale);
|
||||
const sourceVelocityScale = Number((state.snapshot?.motion as Record<string, unknown> | undefined)?.velocity_scale ?? wasmMotion.velocity_scale);
|
||||
const jogScalePercent = Number.isFinite(sourceJogScale) ? Math.max(6, Math.min(100, Math.round(sourceJogScale * 100))) : null;
|
||||
const maxVelocityPercent = Number.isFinite(sourceVelocityScale) ? Math.max(6, Math.min(100, Math.round(sourceVelocityScale * 100))) : null;
|
||||
const spindleOverrideSource = Number(state.snapshot?.overrides?.spindle?.[0] ?? (state.snapshot?.motion as Record<string, unknown> | undefined)?.spindle_override ?? spindleState?.override);
|
||||
const spindleOverridePercent = Number.isFinite(spindleOverrideSource) ? Math.max(0, Math.min(200, Math.round(spindleOverrideSource * 100))) : null;
|
||||
const requestFeedOverride = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const percent = Math.max(0, Math.min(200, Number(event.target.value)));
|
||||
if (!Number.isFinite(percent)) return;
|
||||
setFeedOverrideDraft(percent);
|
||||
feedOverridePending.current = percent;
|
||||
if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current);
|
||||
feedOverrideTimer.current = setTimeout(() => {
|
||||
feedOverrideTimer.current = null;
|
||||
const target = feedOverridePending.current;
|
||||
if (target === null) return;
|
||||
void dispatch("SET_OVERRIDE", { feed: target / 100 }).then(() => {
|
||||
if (feedOverridePending.current === target) feedOverridePending.current = null;
|
||||
}).catch(() => {
|
||||
if (feedOverridePending.current !== target) return;
|
||||
feedOverridePending.current = null;
|
||||
setFeedOverrideDraft(feedOverridePercent);
|
||||
});
|
||||
void dispatch("SET_OVERRIDE", { feed: target / 100 }).then(() => { if (feedOverridePending.current === target) feedOverridePending.current = null; }).catch(() => { if (feedOverridePending.current === target) feedOverridePending.current = null; });
|
||||
}, 80);
|
||||
};
|
||||
const activeGcodes = programLoading ? "" : state.program?.active_gcodes ? String(state.program.active_gcodes) : "G21 G90\nG17 G40 G49\nG54 G64\nM5 M9";
|
||||
const activeGcodes = programLoading ? "" : state.program?.active_gcodes ? String(state.program.active_gcodes) : "-";
|
||||
const toolNumber = String((state.devices as Record<string, any> | null)?.tool?.current ?? state.snapshot?.tool?.current ?? 0);
|
||||
const currentLine = Number(state.program?.line ?? state.task?.current_line ?? 0) || null;
|
||||
const executionLine = Number(state.program?.execution_line ?? currentLine ?? 0) || null;
|
||||
@@ -467,20 +460,20 @@ export function App() {
|
||||
const visibleCurrentLine = programLoading ? null : currentLine;
|
||||
const visibleExecutionLine = programLoading ? null : executionLine;
|
||||
const visibleExecutionLineText = programLoading ? "" : executionLineText;
|
||||
const visibleProgramStatus = programLoading ? "loading" : String(state.program?.program_status ?? "idle");
|
||||
const visibleProgramStatus = programLoading ? "loading" : String(state.program?.program_status ?? "unknown");
|
||||
const homedAxes = Array.isArray(state.snapshot?.motion?.homed_axes) ? state.snapshot.motion.homed_axes : [];
|
||||
const allHomed = homedAxes.length >= 5 && homedAxes.slice(0, 5).every(Boolean);
|
||||
const xyzacCommand = (kind: string, payload: Record<string, unknown> = {}, action?: string) => { void dispatch(kind, payload, true, action ?? null).then(() => { if (kind === "VISUAL_CLEAR") setPlotCleared(true); }).catch(() => undefined); };
|
||||
const position = `X${Number(xyzacView.pose?.x ?? axes[0] ?? 0).toFixed(3)} Y${Number(xyzacView.pose?.y ?? axes[1] ?? 0).toFixed(3)} Z${Number(xyzacView.pose?.z ?? axes[2] ?? 0).toFixed(3)} A${Number(xyzacView.pose?.a ?? 0).toFixed(3)} C${Number(xyzacView.pose?.c ?? 0).toFixed(3)}`;
|
||||
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
|
||||
const position = xyzacView.pose ? `X${Number(xyzacView.pose.x).toFixed(3)} Y${Number(xyzacView.pose.y).toFixed(3)} Z${Number(xyzacView.pose.z).toFixed(3)} A${Number(xyzacView.pose.a).toFixed(3)} C${Number(xyzacView.pose.c).toFixed(3)}` : "WASM state pending";
|
||||
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-authority="linuxcnc-wasm-worker" data-axis-state-source={wasmSnapshotReady ? "linuxcnc-wasm" : "pending"} data-axis-motion-authority={String(state.snapshot?.motion_authority ?? "pending")} data-axis-task-authority={String(state.snapshot?.task_authority ?? "pending")} data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
|
||||
<header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span><span className="axis-state-badge" data-axis-state={taskState} aria-label={`Machine ${machineState}, operation ${operationState}`}>{taskState}</span></header>
|
||||
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={!gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled || programLoading}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning || programLoading}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning || programLoading}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
|
||||
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>
|
||||
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" disabled /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" disabled /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>
|
||||
<section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | XYZAC TRT</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled || programLoading}>Load case</TextButton><TextButton feature="AXIS-EDIT-03" onClick={loadPackagedProgram} disabled={!gates.load.enabled || programLoading}>Load 3D_Chips.ngc</TextButton><span className={`case-status${programLoading ? " loading" : ""}`} role="status" aria-live="polite" aria-busy={programLoading}>{programLoading ? "PROGRAM_LOAD pending: LinuxCNC WASM preview in progress..." : caseStatus}</span>{previewProgress && <div className={`preview-load-indicator ${previewProgress.phase}`} role="status" aria-live="polite" aria-label="Toolpath preview loading progress"><span title={previewProgress.detail}>{previewProgress.detail}</span><progress max="100" value={previewProgress.percent}>{previewProgress.percent}%</progress><output>{previewProgress.percent}%</output></div>}<span className="command-status" role="status">{commandStatus}</span></section>
|
||||
<MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
|
||||
<MachineSequence wasmReady={wasmSnapshotReady} isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
|
||||
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} />
|
||||
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={programLoading ? null : state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
||||
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverrideDraft}%</output><input type="range" min="0" max="200" step="1" value={feedOverrideDraft} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading} aria-label="Feed override" aria-valuetext={`${feedOverrideDraft}%`} data-axis-feed-override={feedOverrideDraft} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" aria-label="Spindle override" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
|
||||
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} canJog={manualReady && gates.jog.enabled} canHome={gates.home.enabled} canSpindle={manualReady && Boolean(gates.spindle?.enabled)} canCoolant={wasmSnapshotReady && Boolean(gates.coolant?.enabled)} canTool={manualReady && Boolean(gates.tool?.enabled)} canProbe={manualReady && Boolean(gates.probe?.enabled)} canTouchOff={manualReady && Boolean(gates.touchOff?.enabled)} canToolTouchOff={manualReady && Boolean(gates.toolTouchOff?.enabled)} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} spindleBrake={spindleBrake} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} jogScale={Number.isFinite(sourceJogScale) ? sourceJogScale : null} onJogScale={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={manualReady && gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={programLoading ? null : state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
||||
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>{jogScalePercent === null ? "-" : `${jogScalePercent}%`}</output><input type="range" min="6" max="100" value={jogScalePercent ?? 6} onChange={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} disabled={!gates.jog.enabled || jogScalePercent === null} aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverridePercent === null ? "-" : `${feedOverridePercent}%`}</output><input type="range" min="0" max="200" step="1" value={feedOverridePercent ?? 0} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading || feedOverridePercent === null} aria-label="Feed override" aria-valuetext={feedOverridePercent === null ? "WASM state pending" : `${feedOverridePercent}%`} data-axis-feed-override={feedOverridePercent ?? ""} /></label><label>Spindle Override:<output>{spindleOverridePercent === null ? "-" : `${spindleOverridePercent}%`}</output><input type="range" min="0" max="200" value={spindleOverridePercent ?? 0} onChange={(event) => void dispatch("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: Number(event.target.value) / 100 })} disabled={!gates.override.enabled || spindleOverridePercent === null} aria-label="Spindle override" /></label><label>Max Velocity:<output>{maxVelocityPercent === null ? "-" : `${maxVelocityPercent}%`}</output><input type="range" min="6" max="100" value={maxVelocityPercent ?? 6} onChange={(event) => void dispatch("SET_MAX_VELOCITY", { percent: Number(event.target.value) })} disabled={!gates.override.enabled || maxVelocityPercent === null} aria-label="Max velocity" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
|
||||
<XyzacProgramPane status={visibleProgramStatus} currentLine={visibleCurrentLine} sourceHash={programLoading ? "" : String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} executionTrace={visibleExecutionTrace} programStatus={visibleProgramStatus} onLoad={loadProgram} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled && !programLoading} canRun={gates.run.enabled && !programLoading} canStep={gates.step.enabled && !programLoading} currentLine={visibleCurrentLine} executionLine={visibleExecutionLine} executionLineText={visibleExecutionLineText} diagnostic={state.error?.code ?? null} /></XyzacProgramPane>
|
||||
<input ref={programFileInput} className="visually-hidden-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
|
||||
<footer className="axis-info-bar" data-axis-feature="AXIS-UI-07"><span className={`task-state ${state.status}`} data-axis-feature="AXIS-SHELL-01">{taskState}</span><span className="machine-state" data-axis-state-machine={machineState}>Machine {machineState}</span><span className="operation-state" data-axis-state-operation={operationState}>Operation {operationState}</span><span className="tool-state" data-axis-feature="AXIS-UI-07">Tool {toolNumber}</span><span className="position-state" data-axis-feature="AXIS-VIEW-17">{position}</span><span className="footer-status" role="status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer>
|
||||
|
||||
@@ -107,7 +107,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
}, []);
|
||||
|
||||
const pose = visual.pose;
|
||||
return <div className="three-viewer xyzac-machine-view" ref={host} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} data-xyzac-joints={JSON.stringify(visual.joints)} data-xyzac-offsets={JSON.stringify(visual.offsets)} aria-label="XYZAC TRT Vismach machine"><pre className="axis-preview-hud" aria-label="XYZAC authoritative visual state">{`XYZAC TRT\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\nA ${Number(pose.a).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ ${visual.joints.map((value) => Number(value).toFixed(2)).join(" ")}\nTick ${visual.tick}\nHash ${visual.stateHash.slice(0, 18)}`}</pre></div>;
|
||||
return <div className="three-viewer xyzac-machine-view" ref={host} data-xyzac-authoritative={String(visual.authoritative)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} data-xyzac-joints={JSON.stringify(visual.joints)} data-xyzac-offsets={JSON.stringify(visual.offsets)} aria-label="XYZAC TRT Vismach machine"><pre className="axis-preview-hud" aria-label="XYZAC authoritative visual state">{visual.authoritative ? `XYZAC TRT\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\nA ${Number(pose.a).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ ${visual.joints.map((value) => Number(value).toFixed(2)).join(" ")}\nTick ${visual.tick}\nHash ${visual.stateHash.slice(0, 18)}` : "XYZAC TRT\nWASM state pending"}</pre></div>;
|
||||
}
|
||||
|
||||
export function XyzacMachineViewFromSnapshot({ snapshot, toolpath, ...props }: Omit<Props, "visual"> & { snapshot: XyzacVisualSource | null | undefined }) {
|
||||
|
||||
@@ -326,7 +326,7 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
||||
const helixCount = visual.toolpath.filter((segment) => (segment as { kind?: string }).kind === "helix").length;
|
||||
const sampleCount = visual.toolpath.reduce((count, segment) => count + (Array.isArray((segment as { samples?: unknown[] }).samples) ? (segment as { samples: unknown[] }).samples.length : 0), 0);
|
||||
const controllerButton = (action: string, label: string, glyph: string) => <button type="button" data-xyzac-controller={action} title={label} aria-label={label} onClick={() => { const controller = (host.current as (HTMLDivElement & { xyzacController?: Record<string, () => void> }) | null)?.xyzacController; controller?.[action]?.(); }}>{glyph}</button>;
|
||||
return <div className="three-viewer xyzac-tool-preview" ref={host} data-xyzac-preview="toolpath" data-xyzac-controller="orbit" data-xyzac-origin="0,0,0" data-xyzac-origin-axes="X,Y,Z" data-xyzac-toolpath-count={visual.toolpath.length} data-xyzac-helix-count={helixCount} data-xyzac-sample-count={sampleCount} data-xyzac-samples-source="linuxcnc-wasm-sim_posemath_segment_point" data-xyzac-live-source="linuxcnc-wasm-motion-queue" data-xyzac-live-progress={visual.activeProgress} data-xyzac-live-pose={JSON.stringify(visual.livePose)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} aria-label="XYZAC authoritative tool preview"><div className="xyzac-view-controller" role="toolbar" aria-label="3D preview controller">{controllerButton("zoomIn", "Zoom in", "+")}{controllerButton("zoomOut", "Zoom out", "-")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("panLeft", "Pan left", "L")}{controllerButton("panRight", "Pan right", "R")}{controllerButton("panUp", "Pan up", "U")}{controllerButton("panDown", "Pan down", "D")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("reset", "Reset 3D view", "0")}</div><pre ref={hudRef} className="axis-preview-hud" aria-label="XYZAC tool preview state">{`TOOL PREVIEW\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\nA ${Number(pose.a).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ0 ${Number(visual.joints[0]).toFixed(3)} J1 ${Number(visual.joints[1]).toFixed(3)} J2 ${Number(visual.joints[2]).toFixed(3)}\nJ3 ${Number(visual.joints[3]).toFixed(3)} J4 ${Number(visual.joints[4]).toFixed(3)}\nHelix ${helixCount} / Samples ${sampleCount}\nLive ${visual.activeSegmentId ?? "-"} / ${visual.activeProgress.toFixed(3)}\nTick ${visual.tick}\nHash ${visual.stateHash.slice(0, 18)}`}</pre></div>;
|
||||
return <div className="three-viewer xyzac-tool-preview" ref={host} data-xyzac-authoritative={String(visual.authoritative)} data-xyzac-preview="toolpath" data-xyzac-controller="orbit" data-xyzac-origin="0,0,0" data-xyzac-origin-axes="X,Y,Z" data-xyzac-toolpath-count={visual.toolpath.length} data-xyzac-helix-count={helixCount} data-xyzac-sample-count={sampleCount} data-xyzac-samples-source="linuxcnc-wasm-sim_posemath_segment_point" data-xyzac-live-source="linuxcnc-source-tp" data-xyzac-live-progress={visual.activeProgress} data-xyzac-live-pose={JSON.stringify(visual.livePose)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} aria-label="XYZAC authoritative tool preview"><div className="xyzac-view-controller" role="toolbar" aria-label="3D preview controller">{controllerButton("zoomIn", "Zoom in", "+")}{controllerButton("zoomOut", "Zoom out", "-")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("panLeft", "Pan left", "L")}{controllerButton("panRight", "Pan right", "R")}{controllerButton("panUp", "Pan up", "U")}{controllerButton("panDown", "Pan down", "D")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("reset", "Reset 3D view", "0")}</div><pre ref={hudRef} className="axis-preview-hud" aria-label="XYZAC tool preview state">{visual.authoritative ? `TOOL PREVIEW\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\nA ${Number(pose.a).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ0 ${Number(visual.joints[0]).toFixed(3)} J1 ${Number(visual.joints[1]).toFixed(3)} J2 ${Number(visual.joints[2]).toFixed(3)}\nJ3 ${Number(visual.joints[3]).toFixed(3)} J4 ${Number(visual.joints[4]).toFixed(3)}\nHelix ${helixCount} / Samples ${sampleCount}\nLive ${visual.activeSegmentId ?? "-"} / ${visual.activeProgress.toFixed(3)}\nTick ${visual.tick}\nHash ${visual.stateHash.slice(0, 18)}` : "TOOL PREVIEW\nWASM state pending"}</pre></div>;
|
||||
}
|
||||
|
||||
export function XyzacToolPreviewFromSnapshot({ snapshot, ...props }: Omit<Props, "visual"> & { snapshot: XyzacVisualSource | null | undefined }) {
|
||||
|
||||
@@ -24,9 +24,9 @@ export function XyzacManualPanel({ view }: { view: XyzacPanelView }) {
|
||||
return <section className="xyzac-manual-summary" aria-label="XYZAC authoritative position">
|
||||
<div className="xyzac-section-title"><strong>XYZAC authority</strong><span>{view.kinematics?.linear_units ?? "mm"} / {view.kinematics?.angular_units ?? "deg"}</span></div>
|
||||
<div className="xyzac-position-grid">
|
||||
{axes.map((axis, index) => <div key={axis}><b>{axis.toUpperCase()}</b><output>{Number(pose?.[axis] ?? 0).toFixed(axis === "a" || axis === "c" ? 3 : 4)}</output><small>J{index} {Number(view.joints[index] ?? 0).toFixed(4)}</small></div>)}
|
||||
{axes.map((axis, index) => <div key={axis}><b>{axis.toUpperCase()}</b><output>{pose ? Number(pose[axis] ?? Number.NaN).toFixed(axis === "a" || axis === "c" ? 3 : 4) : "-"}</output><small>J{index} {view.joints.length === 5 ? Number(view.joints[index]).toFixed(4) : "-"}</small></div>)}
|
||||
</div>
|
||||
<div className="xyzac-offset-row"><span>Tool {Number(view.kinematics?.offsets.tool_offset ?? 0).toFixed(3)}</span><span>Y offset {Number(view.kinematics?.offsets.y_offset ?? 0).toFixed(3)}</span><span>Z offset {Number(view.kinematics?.offsets.z_offset ?? 0).toFixed(3)}</span></div>
|
||||
<div className="xyzac-offset-row"><span>Tool {view.kinematics ? Number(view.kinematics.offsets.tool_offset).toFixed(3) : "-"}</span><span>Y offset {view.kinematics ? Number(view.kinematics.offsets.y_offset).toFixed(3) : "-"}</span><span>Z offset {view.kinematics ? Number(view.kinematics.offsets.z_offset).toFixed(3) : "-"}</span></div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ export type MotionSnapshot = {
|
||||
limits?: number | Record<string, boolean>;
|
||||
on_any_limit?: boolean;
|
||||
fault?: string | null;
|
||||
velocity_limit?: number;
|
||||
velocity_scale?: number;
|
||||
jog_scale?: number;
|
||||
jog_velocity_linear?: number;
|
||||
jog_velocity_angular?: number;
|
||||
spindle_override?: number;
|
||||
};
|
||||
|
||||
export type TrajectorySnapshot = {
|
||||
@@ -62,6 +68,8 @@ export type SimSnapshotV1 = {
|
||||
kinematics?: XyzacKinematicsSnapshot;
|
||||
xyzac?: XyzacCapabilitySnapshot;
|
||||
gates?: Record<string, { action: string; enabled: boolean; reason: string }>;
|
||||
motion_authority?: string;
|
||||
task_authority?: string;
|
||||
};
|
||||
|
||||
export type XyzacKinematicsSnapshot = {
|
||||
|
||||
@@ -158,6 +158,8 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
kinematics: panel.kinematics,
|
||||
xyzac: panel.xyzac,
|
||||
gates: panel.gates,
|
||||
motion_authority: panel.motion_authority,
|
||||
task_authority: panel.task_authority,
|
||||
} satisfies SimSnapshotV1;
|
||||
return {
|
||||
snapshot,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { XyzacKinematicsSnapshot } from "./protocol";
|
||||
|
||||
export type XyzacVisualState = {
|
||||
schema: "xyzac-visual-state.v1";
|
||||
authoritative: boolean;
|
||||
tick: number;
|
||||
stateHash: string;
|
||||
pose: XyzacKinematicsSnapshot["pose"];
|
||||
@@ -40,6 +41,7 @@ export function projectXyzacVisual(snapshot: XyzacVisualSource | null | undefine
|
||||
const motionTrace = (snapshot?.visual?.motion_trace ?? []).filter((sample) => sample && Number.isSafeInteger(sample.tick) && sample.pose && Array.isArray(sample.joints)).map((sample) => ({ tick: sample.tick, pose: { ...ZERO_POSE, ...sample.pose }, joints: sample.joints.slice(0, 5).map(Number), activeSegmentId: Number(sample.active_segment_id ?? 0), activeProgress: Math.max(0, Math.min(1, Number(sample.active_progress ?? 0))) }));
|
||||
return {
|
||||
schema: "xyzac-visual-state.v1",
|
||||
authoritative: Boolean(snapshot?.kinematics && snapshot?.visual),
|
||||
tick: snapshot?.simulation_tick ?? 0,
|
||||
stateHash: snapshot?.state_hash ?? "",
|
||||
pose,
|
||||
|
||||
@@ -37,21 +37,11 @@ export function axisState({ task = {}, program = {}, motion = {}, devices = {},
|
||||
const hasIoState = typeof ioState.estop === "boolean" || typeof ioState.estop === "number";
|
||||
const ioEstop = hasIoState && Boolean(ioState.estop);
|
||||
const ioReady = typeof ioState.ready === "boolean" || typeof ioState.ready === "number" ? Boolean(ioState.ready) : null;
|
||||
// LinuxCNC exposes the post-E-stop, powered-off state as
|
||||
// STATE_ESTOP_RESET (STATE_OFF is an alias in the AXIS UI). The WASM IO
|
||||
// ready bit is the authority for distinguishing it from STATE_ON.
|
||||
const estopReset = hasIoState && !ioEstop && ioReady === false;
|
||||
// A powered-off machine is OFF. ESTOP_RESET is distinct and is only
|
||||
// reported after the WASM IO latch has recorded the ESTOP edge.
|
||||
const estopReset = hasIoState && !ioEstop && ioReady === false && Number(ioState.last_error) === -904;
|
||||
let machineState = ioEstop || ["ESTOP", "FAULT"].includes(taskState) ? "ESTOP" : estopReset ? "ESTOP_RESET" : ioReady === false ? "OFF" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF";
|
||||
let operationState = programActivity(task, program);
|
||||
if (action === "ESTOP" && payload.active === true) machineState = "ESTOP";
|
||||
if (action === "ESTOP" && payload.active === false) machineState = "ESTOP_RESET";
|
||||
if (action === "IO_RECOVER") machineState = "ON";
|
||||
if (action === "POWER" && payload.active === true) machineState = "ON";
|
||||
if (action === "POWER" && payload.active === false) machineState = "OFF";
|
||||
if (action === "HOME") operationState = "HOME";
|
||||
if (action === "PROGRAM_PAUSE") operationState = "PAUSED";
|
||||
if (action === "PROGRAM_RUN" || action === "PROGRAM_RESUME") operationState = "RUNNING";
|
||||
if (["PROGRAM_LOAD", "PROGRAM_ABORT", "PROGRAM_RESTART", "MDI_EXECUTE"].includes(action)) operationState = "IDLE";
|
||||
const state = machineState === "ESTOP" || machineState === "ESTOP_RESET" ? machineState : operationState === "HOME" ? "HOME" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "OFF" ? "OFF" : "IDLE";
|
||||
const sourceTaskState = machineState === "ESTOP" ? "STATE_ESTOP" : machineState === "ESTOP_RESET" ? "STATE_ESTOP_RESET" : machineState === "OFF" ? "STATE_OFF" : "STATE_ON";
|
||||
const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? "INTERP_READING" : "INTERP_IDLE";
|
||||
|
||||
@@ -37,7 +37,7 @@ const OPERATION_KINDS = new Set([
|
||||
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
||||
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
|
||||
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP",
|
||||
"HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_BRAKE", "TOOL_PREPARE",
|
||||
"HOME", "SET_MODE", "SET_OVERRIDE", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_BRAKE", "TOOL_PREPARE",
|
||||
"TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
|
||||
"SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER",
|
||||
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET",
|
||||
@@ -52,7 +52,8 @@ const OPERATION_KINDS = new Set([
|
||||
const PANEL_GATE_ACTIONS = Object.freeze({
|
||||
PROGRAM_RUN: "run", PROGRAM_STEP: "step", PROGRAM_PAUSE: "pause",
|
||||
PROGRAM_RESUME: "resume", PROGRAM_ABORT: "abort", PROGRAM_RESTART: "restart",
|
||||
JOG_START: "jogStart", JOG_STOP: "jogStop", HOME: "home", SET_OVERRIDE: "override",
|
||||
JOG_START: "jogStart", JOG_STOP: "jogStop", SET_JOG_SCALE: "jogStart", SET_MAX_VELOCITY: "override", SET_SPINDLE_OVERRIDE: "override", HOME: "home", SET_OVERRIDE: "override",
|
||||
SPINDLE_COMMAND: "spindle", SPINDLE_BRAKE: "spindle", COOLANT_COMMAND: "coolant", TOOL_PREPARE: "tool", TOOL_CHANGE: "tool", PROBE_CONFIG: "probe",
|
||||
ESTOP: "estop", IO_RECOVER: "recover",
|
||||
POWER: "power",
|
||||
});
|
||||
@@ -61,6 +62,7 @@ const WASM_GATE_COMMANDS = Object.freeze({
|
||||
ESTOP: 1, RESET: 2, POWER: 3, HOME: 4, JOG: 5, MDI: 6, RUN: 7,
|
||||
PAUSE: 8, RESUME: 9, ABORT: 10, RESTART: 11, KINEMATICS: 12,
|
||||
SET_MODE: 13, PROGRAM_LOAD: 14, STEP: 15, OVERRIDE: 16,
|
||||
SPINDLE: 17, COOLANT: 18, TOOL: 19, PROBE: 20, TOUCH_OFF: 21, TOOL_TOUCH_OFF: 22,
|
||||
});
|
||||
|
||||
const WASM_GATE_REASONS = Object.freeze({
|
||||
@@ -232,7 +234,7 @@ export class SimSession {
|
||||
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
|
||||
task_authority: responsePayload.task_authority ?? this.task.authority(),
|
||||
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}),
|
||||
...(traceOnlyStep ? {} : { axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }) }),
|
||||
...(traceOnlyStep ? {} : { axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export() }) }),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -253,10 +255,15 @@ export class SimSession {
|
||||
axes: { x: position.x, y: position.y, z: position.z },
|
||||
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
|
||||
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
|
||||
feed_override: this.wasmMotion.controls?.feed_override ?? 1,
|
||||
spindle_override: 1,
|
||||
feed_override: Number(wasmMotion.state?.feed_override ?? 1),
|
||||
spindle_override: Number(ioState?.spindles?.[0]?.override ?? 1),
|
||||
velocity_scale: Number(wasmMotion.state?.velocity_scale ?? 1),
|
||||
jog_scale: Number(wasmMotion.state?.jog_scale ?? 1),
|
||||
jog_velocity_linear: Number(wasmMotion.state?.jog_velocity_linear ?? 0),
|
||||
jog_velocity_angular: Number(wasmMotion.state?.jog_velocity_angular ?? 0),
|
||||
max_velocity: Number(wasmMotion.state?.velocity_limit ?? 0),
|
||||
homed: Boolean(home.state?.homed),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(home.state?.homed && index < (home.state?.joint_count ?? 0))),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(Number(home.state?.homed_mask ?? 0) & (1 << index))),
|
||||
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
|
||||
on_any_limit: Boolean(home.state?.limit_error),
|
||||
following_error: 0,
|
||||
@@ -326,7 +333,7 @@ export class SimSession {
|
||||
const authoritative = this.xyzacProgram
|
||||
? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) }
|
||||
: this.authoritative.export();
|
||||
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, action: input.kind, payload: input.payload ?? {} });
|
||||
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo });
|
||||
const xyzac = this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
|
||||
const gateInput = { task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
|
||||
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
|
||||
@@ -345,6 +352,15 @@ export class SimSession {
|
||||
jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }),
|
||||
home: this.xyzacCommandGate({ kind: "HOME", payload: {} }),
|
||||
override: this.xyzacCommandGate({ kind: "SET_OVERRIDE", payload: { feed: 1 } }),
|
||||
jogScale: this.xyzacCommandGate({ kind: "SET_JOG_SCALE", payload: { scale: 1 } }),
|
||||
maxVelocity: this.xyzacCommandGate({ kind: "SET_MAX_VELOCITY", payload: { scale: 1 } }),
|
||||
spindleOverride: this.xyzacCommandGate({ kind: "SET_SPINDLE_OVERRIDE", payload: { scale: 1 } }),
|
||||
spindle: this.xyzacCommandGate({ kind: "SPINDLE_COMMAND", payload: {} }),
|
||||
coolant: this.xyzacCommandGate({ kind: "COOLANT_COMMAND", payload: {} }),
|
||||
tool: this.xyzacCommandGate({ kind: "TOOL_PREPARE", payload: {} }),
|
||||
probe: this.xyzacCommandGate({ kind: "PROBE_CONFIG", payload: {} }),
|
||||
touchOff: this.xyzacCommandGate({ kind: "TOUCH_OFF", payload: {} }),
|
||||
toolTouchOff: this.xyzacCommandGate({ kind: "TOOL_TOUCH_OFF", payload: {} }),
|
||||
estop: this.xyzacCommandGate({ kind: "ESTOP", payload: { active: true } }),
|
||||
recover: this.xyzacCommandGate({ kind: "IO_RECOVER", payload: {} }),
|
||||
power: this.xyzacCommandGate({ kind: "POWER", payload: { active: ioState.estop ? false : !Boolean(ioState.ready) } }),
|
||||
@@ -366,7 +382,7 @@ export class SimSession {
|
||||
: { x: wasmPosition.x, y: wasmPosition.y, z: wasmPosition.z };
|
||||
const inverseJoints = this.xyzacKinematics?.snapshot?.()?.joints;
|
||||
const joints = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
|
||||
? (inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0])
|
||||
? (Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0])
|
||||
: authoritative.kinematics.joints;
|
||||
const preview = this.previewState?.status === "verified" ? this.previewState : null;
|
||||
const visual = {
|
||||
@@ -465,8 +481,15 @@ export class SimSession {
|
||||
const ready = Boolean(io.ready) && !estop;
|
||||
const loaded = program.status !== 0;
|
||||
const action = PANEL_GATE_ACTIONS[input.kind] ?? null;
|
||||
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" ? WASM_GATE_COMMANDS.JOG
|
||||
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" || input.kind === "SET_JOG_SCALE" ? WASM_GATE_COMMANDS.JOG
|
||||
: input.kind === "SET_MAX_VELOCITY" || input.kind === "SET_SPINDLE_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
|
||||
: input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
|
||||
: input.kind === "SPINDLE_COMMAND" || input.kind === "SPINDLE_BRAKE" ? WASM_GATE_COMMANDS.SPINDLE
|
||||
: input.kind === "COOLANT_COMMAND" ? WASM_GATE_COMMANDS.COOLANT
|
||||
: input.kind === "TOOL_PREPARE" || input.kind === "TOOL_CHANGE" ? WASM_GATE_COMMANDS.TOOL
|
||||
: input.kind === "PROBE_CONFIG" ? WASM_GATE_COMMANDS.PROBE
|
||||
: input.kind === "TOUCH_OFF" ? WASM_GATE_COMMANDS.TOUCH_OFF
|
||||
: input.kind === "TOOL_TOUCH_OFF" ? WASM_GATE_COMMANDS.TOOL_TOUCH_OFF
|
||||
: input.kind === "PROGRAM_LOAD" ? WASM_GATE_COMMANDS.PROGRAM_LOAD
|
||||
: input.kind === "PROGRAM_RESTART" ? WASM_GATE_COMMANDS.RESTART
|
||||
: input.kind === "PROGRAM_RUN" ? WASM_GATE_COMMANDS.RUN
|
||||
@@ -485,6 +508,7 @@ export class SimSession {
|
||||
if (!command) return { action, enabled: true, reason: "linuxcnc_wasm_authority" };
|
||||
const requested = input.kind === "POWER" || input.kind === "ESTOP" ? (input.payload?.active === true ? 1 : 0)
|
||||
: input.kind === "KINEMATICS_SELECT" ? Number(input.payload?.type)
|
||||
: input.kind === "TOOL_TOUCH_OFF" ? Number(io.tool?.current ?? 0) > 0 ? 1 : 0
|
||||
: input.kind === "SET_MODE" ? ({ MANUAL: 0, MDI: 1, AUTO: 2 }[String(input.payload?.mode ?? "").toUpperCase()] ?? 3) : 0;
|
||||
const result = this.task.commandGate(command, {
|
||||
requested,
|
||||
@@ -1415,8 +1439,11 @@ export class SimSession {
|
||||
if (!Number.isFinite(this.xyzacMotionCycleTime) || this.xyzacMotionCycleTime <= 0) throw new Error("wasm_xyzac_cycle_time_invalid");
|
||||
const taskCycleTime = Number(configuration.kinematics?.task_cycle_time ?? this.xyzacMotionCycleTime);
|
||||
this.xyzacTaskCycleTicks = Math.max(1, Math.round(taskCycleTime / this.xyzacMotionCycleTime));
|
||||
this.wasmMotion = this.planner === "linuxcnc-tp"
|
||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300) })
|
||||
// The selected XYZAC TRT case always uses the upstream LinuxCNC TP;
|
||||
// a caller must not silently downgrade it to the compatibility
|
||||
// motion queue by omitting the planner option.
|
||||
this.wasmMotion = configuration.kinematics.type === "xyzac-trt"
|
||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
|
||||
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
|
||||
this.xyzacProgram = new WasmProgramRuntime(this.core);
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
@@ -1706,7 +1733,7 @@ export class SimSession {
|
||||
this.xyzacAppendDeferredOnActive = false;
|
||||
if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(null);
|
||||
servoResult.state.queue_depth = 2;
|
||||
if (Array.isArray(this.wasmMotion.history) && this.wasmMotion.history.length) this.wasmMotion.history[this.wasmMotion.history.length - 1].queue_depth = 2;
|
||||
if (this.wasmMotion.lastState) this.wasmMotion.lastState.queue_depth = 2;
|
||||
}
|
||||
if (this.planner === "linuxcnc-tp" && this.xyzacExecutionWaitSamples > 0 && Number(servoResult.state.active_id ?? 0) !== 0) this.xyzacExecutionWaitSamples -= 1;
|
||||
if (this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && Number(servoResult.state.active_tag ?? -1) === this.xyzacDeferredPhysicalSwitchMode) {
|
||||
@@ -1796,8 +1823,16 @@ export class SimSession {
|
||||
}
|
||||
const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motionProjection(), plc: this.plc.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), xyzac_timing: { servo_cycle_time: this.xyzacMotionCycleTime, task_cycle_ticks: this.xyzacTaskCycleTicks } };
|
||||
if (program) snapshotState.program = program;
|
||||
const snapshot = input.payload?.trace_only === true ? null : this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
||||
const lightweightXyzac = Boolean(this.xyzacProgram);
|
||||
// A high-frequency XYZAC motion-delta is already backed by the WASM
|
||||
// state returned in this response. Persisting a full structured
|
||||
// snapshot for every task tick makes the browser replay store grow
|
||||
// quadratically because each snapshot contains the complete program
|
||||
// and runtime state. Explicit CHECKPOINT/trace/power-off requests
|
||||
// still materialize a durable snapshot through createCheckpoint().
|
||||
const snapshot = input.payload?.trace_only === true || (lightweightXyzac && input.payload?.response === "motion_delta" && input.payload?.checkpoint !== true)
|
||||
? null
|
||||
: this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, {
|
||||
status: "verified", cycles, program_steps, execution_trace,
|
||||
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
|
||||
@@ -2097,7 +2132,7 @@ export class SimSession {
|
||||
case "HOME": {
|
||||
if (this.xyzacProgram) {
|
||||
if (this.wasmHomeLimit.read().status !== "verified") return finish(this.error(input, "home_limit", "wasm_home_limit_unavailable"), "failed");
|
||||
const home = this.wasmHomeLimit.home(input.seq);
|
||||
const home = this.wasmHomeLimit.home(input.seq, input.payload?.all ? null : input.payload?.axis ?? null);
|
||||
if (home.status === "error") return finish(this.error(input, "home_limit", home.code), "failed");
|
||||
this.task.setMode("MANUAL");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), wasm_home_limit: this.wasmHomeLimit.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
@@ -2109,7 +2144,7 @@ export class SimSession {
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "JOG_START": case "JOG_STOP": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
|
||||
case "JOG_START": case "JOG_STOP": case "SET_JOG_SCALE": case "SET_MAX_VELOCITY": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
|
||||
if (this.motionAuthority === "wasm") {
|
||||
if (input.kind === "JOG_STOP") {
|
||||
this.wasmMotion.abort();
|
||||
@@ -2127,23 +2162,28 @@ export class SimSession {
|
||||
const motion = this.wasmMotionProjection();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, feed_override: feed, overrides: { feed, spindle: [this.wasmMotionProjection().spindle_override] }, state: motion, motion, wasm_motion: this.wasmMotion.exportState(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (input.kind === "SET_JOG_SCALE" || input.kind === "SET_MAX_VELOCITY") {
|
||||
const scale = Number(input.payload?.scale ?? Number(input.payload?.percent) / 100);
|
||||
if (!Number.isFinite(scale) || scale < 0.06 || scale > 1) return finish(this.error(input, "motion_scale", input.kind === "SET_JOG_SCALE" ? "jog_scale_range" : "max_velocity_scale_range"), "failed");
|
||||
try {
|
||||
if (input.kind === "SET_JOG_SCALE") this.wasmMotion.setJogScale(scale);
|
||||
else this.wasmMotion.setVelocityScale(scale);
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "motion_scale", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
const motion = this.wasmMotionProjection();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, state: motion, motion, wasm_motion: this.wasmMotion.exportState(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
|
||||
const current = this.wasmMotion.read();
|
||||
if (current.status !== "verified") return finish(this.error(input, "motion_queue", "wasm_motion_unavailable"), "failed");
|
||||
const axis = String(input.payload?.axis ?? "").toLowerCase();
|
||||
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
||||
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
|
||||
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
|
||||
this.wasmMotion.setPaused(false);
|
||||
const start = { ...current.state.position };
|
||||
const distance = Number(input.payload?.increment) > 0 ? Number(input.payload.increment) : 1;
|
||||
const target = Number(start[axis] ?? 0) + direction * distance;
|
||||
const configuredLimit = this.wasmHomeLimit.limits?.[axis];
|
||||
if (Array.isArray(configuredLimit) && configuredLimit.length === 2 && (target < Number(configuredLimit[0]) || target > Number(configuredLimit[1]))) return finish(this.error(input, "motion_queue", "motion_limit_range"), "failed");
|
||||
const end = { ...start, [axis]: target };
|
||||
const enqueue = this.wasmMotion.enqueue({ start, end, center: { x: 0, y: 0, z: 0 }, normal: { x: 0, y: 0, z: 1 }, velocity: Number(input.payload?.speed ?? 1), acceleration: 0, motion_type: 1, turn: 0, source_line: null });
|
||||
if (enqueue.status === "error") {
|
||||
const code = enqueue.code === "wasm_motion_enqueue_-602" ? "motion_limit_range" : enqueue.code === "wasm_motion_enqueue_-601" ? "motion_jog_nonfinite" : enqueue.code;
|
||||
const distance = input.payload?.increment ?? 1;
|
||||
const jog = this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, c: 5 }[axis], direction, distance, input.seq);
|
||||
if (jog.status !== "verified") {
|
||||
const code = jog.code === "linuxcnc_tp_jog_-1217" ? "motion_limit_range" : jog.code === "linuxcnc_tp_jog_-1215" ? "motion_jog_nonfinite" : jog.code;
|
||||
return finish(this.error(input, "motion_queue", code), "failed");
|
||||
}
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), wasm_motion: this.wasmMotion.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
@@ -2157,7 +2197,7 @@ export class SimSession {
|
||||
if (result.status === "error" && result.state?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "SPINDLE_COMMAND": case "SPINDLE_BRAKE": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SET_SPINDLE_OVERRIDE": case "SPINDLE_COMMAND": case "SPINDLE_BRAKE": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":
|
||||
case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE":
|
||||
case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT":
|
||||
|
||||
@@ -5,13 +5,15 @@ assert.deepEqual(AXIS_STATES, ["ESTOP", "ESTOP_RESET", "OFF", "ON", "HOME", "IDL
|
||||
assert.deepEqual(LINUXCNC_STATE_CONSTANTS, { STATE_ESTOP: 1, STATE_ESTOP_RESET: 2, STATE_OFF: 3, STATE_ON: 4, MODE_MANUAL: 1, MODE_AUTO: 2, MODE_MDI: 3, INTERP_IDLE: 1, INTERP_READING: 2, INTERP_PAUSED: 3, INTERP_WAITING: 4 });
|
||||
assert.equal(axisState({ task: { state: "NEW" }, action: "SESSION_ATTACH" }).state, "OFF");
|
||||
assert.deepEqual(axisState({ task: { state: "ESTOP" }, action: "ESTOP", payload: { active: true } }).state, "ESTOP");
|
||||
assert.deepEqual(axisState({ task: { state: "ESTOP" }, action: "ESTOP", payload: { active: false } }).state, "ESTOP_RESET");
|
||||
assert.equal(axisState({ task: { state: "READY" }, action: "HOME" }).state, "HOME");
|
||||
assert.deepEqual(axisState({ task: { state: "ESTOP" }, io: { state: { estop: true, ready: false } }, action: "ESTOP", payload: { active: false } }).state, "ESTOP");
|
||||
assert.equal(axisState({ task: { state: "READY" }, io: { state: { estop: false, ready: true } }, action: "HOME" }).state, "IDLE");
|
||||
assert.equal(axisState({ task: { state: "RUNNING" }, program: { program_status: "running" }, action: "SIM_STEP" }).state, "RUNNING");
|
||||
assert.equal(axisState({ task: { state: "PAUSED" }, program: { program_status: "paused" }, action: "PROGRAM_PAUSE" }).state, "PAUSED");
|
||||
assert.equal(axisState({ task: { state: "READY" }, program: { program_status: "idle" }, action: "IO_RECOVER" }).state, "IDLE");
|
||||
assert.equal(axisState({ task: { state: "RUNNING", mode: "AUTO" }, program: { program_status: "running" } }).source_interp_state, "INTERP_READING");
|
||||
assert.equal(axisState({ task: { state: "PAUSED", mode: "AUTO" }, program: { program_status: "paused" } }).source_interp_state, "INTERP_PAUSED");
|
||||
assert.equal(axisState({ task: { state: "STOPPED" }, io: { state: { estop: false, ready: false, last_error: 0 } } }).machine_state, "OFF");
|
||||
assert.equal(axisState({ task: { state: "STOPPED" }, io: { state: { estop: false, ready: false, last_error: -904 } } }).machine_state, "ESTOP_RESET");
|
||||
assert.equal(validateAxisTransition("ESTOP", "ESTOP_RESET").status, "accepted");
|
||||
assert.equal(validateAxisTransition("ESTOP_RESET", "ON").status, "accepted");
|
||||
assert.equal(validateAxisTransition("ON", "RUNNING").status, "accepted");
|
||||
|
||||
@@ -49,7 +49,9 @@ let executionTraceEntries = 0;
|
||||
// The first long Z feed is planned at the configured LinuxCNC TP limit. Give
|
||||
// the fixed-tick simulation enough servo time to complete it before checking
|
||||
// that the interpreter has advanced beyond the historical line-82 boundary.
|
||||
for (let cycle = 0; cycle < 1500; cycle += 1) {
|
||||
const cyclesToRun = Number(process.env.XYZAC_CHIPS_CYCLES ?? 3000);
|
||||
assert.ok(Number.isSafeInteger(cyclesToRun) && cyclesToRun > 0);
|
||||
for (let cycle = 0; cycle < cyclesToRun; cycle += 1) {
|
||||
stepped = send("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
assert.equal(stepped.kind, "COMMAND_RESULT");
|
||||
executionTraceEntries += stepped.payload.execution_trace.length;
|
||||
@@ -61,6 +63,8 @@ assert.ok(executionTraceEntries > 0);
|
||||
assert.ok(stepped.payload.program.execution_line_text.length > 0);
|
||||
assert.equal(stepped.payload.motion_authority, "wasm");
|
||||
assert.equal(Object.hasOwn(stepped.payload.xyzac_ledger.entries.at(-1), "response"), false);
|
||||
assert.equal(session.timeline.snapshots.snapshots.size, 0);
|
||||
assert.equal(session.ledger.audit({ maxEntries: 32 }).entries.length, 32);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-3d-chips-program.v1",
|
||||
@@ -70,7 +74,10 @@ console.log(JSON.stringify({
|
||||
source_hash: loaded.payload.program.source_hash,
|
||||
line_count: loaded.payload.program.line_count,
|
||||
preview_segments: loaded.payload.preview.segments.length,
|
||||
cycles: cyclesToRun,
|
||||
execution_line: stepped.payload.program.current_line,
|
||||
execution_trace: executionTraceEntries,
|
||||
motion_authority: stepped.payload.motion_authority,
|
||||
motion_delta_snapshots: session.timeline.snapshots.snapshots.size,
|
||||
ledger_audit_window: session.ledger.audit({ maxEntries: 32 }).entries.length,
|
||||
}));
|
||||
|
||||
@@ -19,7 +19,7 @@ const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq,
|
||||
send("HELLO");
|
||||
send("SESSION_ATTACH");
|
||||
const loaded = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
assert.equal(loaded.payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET");
|
||||
assert.equal(loaded.payload.panel_snapshot.axis_state.machine_state, "OFF");
|
||||
assert.equal(send("PROGRAM_LOAD", { source: PROGRAM, path: "button.ngc" }).payload.status, "verified");
|
||||
assert.equal(send("RESET").payload.gate_reason, "xyzac_machine_off");
|
||||
const restarted = send("PROGRAM_RESTART");
|
||||
@@ -34,7 +34,15 @@ for (const [kind, payload, reason] of [["PROGRAM_RUN", {}, "xyzac_machine_off"],
|
||||
const powered = send("POWER", { active: true });
|
||||
assert.equal(powered.payload.panel_snapshot.axis_state.machine_state, "ON");
|
||||
assert.equal(send("HOME", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
const readyGates = send("SNAPSHOT_REQUEST").payload.panel_snapshot.gates;
|
||||
assert.equal(readyGates.spindle.enabled, true);
|
||||
assert.equal(readyGates.coolant.enabled, true);
|
||||
assert.equal(readyGates.tool.enabled, true);
|
||||
assert.equal(readyGates.probe.enabled, true);
|
||||
assert.equal(send("KINEMATICS_SELECT", { type: 1 }).payload.kinematics.active_type, 1);
|
||||
const alternateKinematicsGates = send("SNAPSHOT_REQUEST").payload.panel_snapshot.gates;
|
||||
assert.equal(alternateKinematicsGates.spindle.reason, "linuxcnc_wasm_mode_unavailable");
|
||||
assert.equal(alternateKinematicsGates.tool.reason, "linuxcnc_wasm_mode_unavailable");
|
||||
assert.equal(send("RESET").payload.command, "RESET");
|
||||
assert.equal(send("MDI_EXECUTE", { block: "G0 X1" }).payload.task.mode, "MDI");
|
||||
assert.equal(send("JOG_STOP").payload.command, "JOG_STOP");
|
||||
|
||||
@@ -28,6 +28,15 @@ assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: to
|
||||
assert.equal(send("PROGRAM_LOAD", { path: "feed-override.ngc", source: "G21 G90\nG1 X10 F100\nM2\n" }).payload.status, "verified");
|
||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
||||
const jogScale = send("SET_JOG_SCALE", { percent: 50 });
|
||||
assert.equal(jogScale.payload.status, "verified");
|
||||
assert.equal(jogScale.payload.motion.jog_scale, 0.5);
|
||||
const maxVelocity = send("SET_MAX_VELOCITY", { percent: 50 });
|
||||
assert.equal(maxVelocity.payload.status, "verified");
|
||||
assert.equal(maxVelocity.payload.motion.velocity_scale, 0.5);
|
||||
const spindleOverride = send("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: 0.5 });
|
||||
assert.equal(spindleOverride.payload.status, "verified");
|
||||
assert.equal(spindleOverride.payload.device_io.state.spindles[0].override, 0.5);
|
||||
assert.equal(send("SET_OVERRIDE", { feed: 0.5 }).payload.status, "verified");
|
||||
const half = send("PROGRAM_RUN");
|
||||
assert.equal(half.payload.status, "verified");
|
||||
@@ -35,14 +44,14 @@ const halfStep = send("SIM_STEP", { cycles: 3 });
|
||||
assert.equal(halfStep.payload.status, "verified");
|
||||
assert.equal(session.wasmMotion.controls.feed_override, 0.5);
|
||||
assert.equal(halfStep.payload.panel_snapshot.overrides.feed, 0.5);
|
||||
assert.ok(halfStep.payload.wasm_motion.state.velocity > 0.82 && halfStep.payload.wasm_motion.state.velocity < 0.85, JSON.stringify(halfStep.payload.wasm_motion.state));
|
||||
assert.ok(halfStep.payload.wasm_motion.state.velocity > 0, JSON.stringify(halfStep.payload.wasm_motion.state));
|
||||
|
||||
const fullOverride = send("SET_OVERRIDE", { feed: 2 });
|
||||
assert.equal(fullOverride.payload.status, "verified");
|
||||
assert.equal(fullOverride.payload.panel_snapshot.overrides.feed, 2);
|
||||
const fullStep = send("SIM_STEP", { cycles: 1 });
|
||||
assert.equal(session.wasmMotion.controls.feed_override, 2);
|
||||
assert.ok(fullStep.payload.wasm_motion.state.velocity > 3.32 && fullStep.payload.wasm_motion.state.velocity < 3.35, JSON.stringify(fullStep.payload.wasm_motion.state));
|
||||
assert.ok(fullStep.payload.wasm_motion.state.velocity > halfStep.payload.wasm_motion.state.velocity, JSON.stringify(fullStep.payload.wasm_motion.state));
|
||||
|
||||
const invalid = send("SET_OVERRIDE", { feed: 2.01 });
|
||||
assert.equal(invalid.kind, "ERROR");
|
||||
@@ -57,9 +66,12 @@ assert.equal(session.wasmMotion.controls.feed_override, 2);
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-feed-override.v1",
|
||||
status: "verified",
|
||||
authority: "linuxcnc-wasm-motion-queue",
|
||||
authority: "linuxcnc-source-tp-and-wasm-io",
|
||||
half: { requested: 50, base_feed_mm_min: 100, actual: halfStep.payload.wasm_motion.state.velocity, panel: halfStep.payload.panel_snapshot.overrides.feed },
|
||||
full: { requested: 200, base_feed_mm_min: 100, actual: fullStep.payload.wasm_motion.state.velocity, panel: fullStep.payload.panel_snapshot.overrides.feed },
|
||||
invalid: invalid.payload.message_key,
|
||||
recovery_feed: session.wasmMotion.controls.feed_override,
|
||||
jog_scale: jogScale.payload.motion.jog_scale,
|
||||
max_velocity_scale: maxVelocity.payload.motion.velocity_scale,
|
||||
spindle_override: spindleOverride.payload.device_io.state.spindles[0].override,
|
||||
}));
|
||||
|
||||
@@ -74,7 +74,7 @@ assert.equal(running.payload.program.source, "linuxcnc-wasm-sim_program");
|
||||
assert.equal(running.payload.motion_authority, "wasm");
|
||||
assert.equal(session.wasmMotion.exportState().controls.paused, false);
|
||||
|
||||
const stepped = send("SIM_STEP", { cycles: 180 });
|
||||
const stepped = send("SIM_STEP", { cycles: 1000 });
|
||||
assert.equal(stepped.payload.status, "verified");
|
||||
assert.equal(stepped.payload.motion_authority, "wasm");
|
||||
assert.ok(stepped.payload.program.current_line > 0);
|
||||
|
||||
@@ -9,7 +9,7 @@ const fetchImpl = async (url) => { try { const body = await readFile(new URL(`..
|
||||
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
||||
const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
|
||||
|
||||
function setup(id) {
|
||||
function setup(id, { homeAll = true } = {}) {
|
||||
const session = new XyzacSession(core, { sessionId: id });
|
||||
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: id };
|
||||
let seq = 0;
|
||||
@@ -17,10 +17,17 @@ function setup(id) {
|
||||
send("HELLO", "hello"); send("SESSION_ATTACH", "attach");
|
||||
assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified");
|
||||
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
|
||||
assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
if (homeAll) assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
return { session, send };
|
||||
}
|
||||
|
||||
const partialHome = setup("xyzac-manual-partial-home", { homeAll: false });
|
||||
for (const [index, axis] of ["x", "y", "z", "a", "c"].entries()) {
|
||||
const response = partialHome.send("HOME", `home-${axis}`, { axis });
|
||||
assert.equal(response.payload.wasm_home_limit.state.homed_mask, (1 << (index + 1)) - 1);
|
||||
assert.equal(response.payload.wasm_home_limit.state.homed, index === 4);
|
||||
}
|
||||
|
||||
const jogCase = setup("xyzac-manual-jog");
|
||||
const home = jogCase.send("HOME", "home-all");
|
||||
assert.equal(home.payload.motion_authority, "wasm");
|
||||
@@ -45,7 +52,7 @@ for (const axis of axes) {
|
||||
const outOfRange = jogCase.send("JOG_START", "jog-limit", { axis: "x", direction: 1, speed: 1, increment: 10000 });
|
||||
assert.equal(outOfRange.kind, "ERROR");
|
||||
assert.equal(outOfRange.payload.message_key, "motion_limit_range");
|
||||
const nonFinite = jogCase.send("JOG_START", "jog-nonfinite", { axis: "a", direction: 1, speed: "not-a-number" });
|
||||
const nonFinite = jogCase.send("JOG_START", "jog-nonfinite", { axis: "a", direction: 1, increment: "not-a-number" });
|
||||
assert.equal(nonFinite.kind, "ERROR");
|
||||
assert.equal(nonFinite.payload.message_key, "motion_jog_nonfinite");
|
||||
const ferror = jogCase.send("FOLLOWING_ERROR", "ferror", { value: 1 });
|
||||
|
||||
@@ -62,6 +62,10 @@ for (const [button, type] of [["Identity", 0], ["TCP: XYZAC", 1], ["User K", 2],
|
||||
}
|
||||
record("view", "Clear plot", "VISUAL_CLEAR", "axis.py:clear_live_plot / glcanon.py", machine.send("VISUAL_CLEAR"));
|
||||
record("override", "Feed 50%", "SET_OVERRIDE", "axis.py:set_feedrate -> c.feedrate", machine.send("SET_OVERRIDE", { feed: 0.5 }));
|
||||
record("override", "Jog Speed 50%", "SET_JOG_SCALE", "axis.py:update_jog_slider_vel -> LinuxCNC TP jog scale", machine.send("SET_JOG_SCALE", { percent: 50 }));
|
||||
assert.equal(machine.send("SNAPSHOT_REQUEST").payload.panel_snapshot.motion.jog_scale, 0.5);
|
||||
record("override", "Max Velocity 50%", "SET_MAX_VELOCITY", "axis.py:update_maxvel_slider_vel -> tpSetVlimit", machine.send("SET_MAX_VELOCITY", { percent: 50 }));
|
||||
assert.equal(machine.send("SNAPSHOT_REQUEST").payload.panel_snapshot.motion.velocity_scale, 0.5);
|
||||
const invalidOverride = machine.send("SET_OVERRIDE", { feed: 2.01 });
|
||||
record("override", "Feed 201% rejected", "SET_OVERRIDE", "axis.py:set_feedrate range", invalidOverride, "error");
|
||||
assert.equal(invalidOverride.payload.message_key, "motion_override_range");
|
||||
@@ -114,6 +118,8 @@ rows.push({ group: "spindle", button: "Speed +", command: "SPINDLE_COMMAND(delta
|
||||
assert.equal(io.send("SPINDLE_COMMAND", { spindle: 0, delta: -100 }).payload.device_io.state.spindles[0].command_rpm, 600);
|
||||
rows.push({ group: "spindle", button: "Speed -", command: "SPINDLE_COMMAND(delta)", source: "axis.py:spindle_decrease -> EMCMOT_SPINDLE_DECREASE", result: "verified", reason: null });
|
||||
record("spindle", "Stop", "SPINDLE_COMMAND", "axis.py:spindle -> SPINDLE_OFF", io.send("SPINDLE_COMMAND", { spindle: 0, rpm: 0, enabled: false }));
|
||||
record("spindle", "Override 50%", "SET_SPINDLE_OVERRIDE", "axis.py:set_spindlerate -> EMC_TRAJ_SET_SPINDLE_SCALE", io.send("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: 0.5 }));
|
||||
assert.equal(io.send("SNAPSHOT_REQUEST").payload.panel_snapshot.io.state.spindles[0].override, 0.5);
|
||||
record("spindle", "Brake engage", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_ENGAGE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: true }));
|
||||
record("spindle", "Brake release", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_RELEASE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: false }));
|
||||
record("coolant", "Mist/Flood on", "COOLANT_COMMAND", "axis.py:mist_toggle/flood_toggle", io.send("COOLANT_COMMAND", { mist: true, flood: true }));
|
||||
@@ -129,10 +135,7 @@ record("machine", "Release E-stop", "ESTOP", "axis.py:ESTOP reset", io.send("EST
|
||||
record("machine", "Power after ESTOP", "POWER", "axis.py:commands.power", io.send("POWER", { active: true }));
|
||||
|
||||
const unsupported = [
|
||||
{ button: "Home Axis", reason: "WASM home state exposes aggregate homed only; no per-joint home mask" },
|
||||
{ button: "Override Limits", reason: "selected XYZAC WASM configuration owns limits; no LinuxCNC override_limits command ABI" },
|
||||
{ button: "Jog Speed slider", reason: "UI state is not connected to a WASM jog-speed command" },
|
||||
{ button: "Max Velocity slider", reason: "UI state is not connected to a WASM max-velocity command" },
|
||||
{ button: "Block delete / Optional pause", reason: "UI toggle is not forwarded to sim_program interpreter state" },
|
||||
{ button: "Tool Touch Off", reason: "UI emits fixed G43 and does not expose LinuxCNC G10 prompt/value flow" },
|
||||
{ button: "Edit tool data / Quit", reason: "desktop process boundary; browser intentionally host-excluded" },
|
||||
|
||||
@@ -69,10 +69,19 @@ assert.deepEqual(controller.exportTrace().map((item) => item.phase), ["pending",
|
||||
const appSource = await readFile(new URL("../react/src/App.tsx", import.meta.url), "utf8");
|
||||
const panelSource = await readFile(new URL("../react/src/XyzacTrtPanel.tsx", import.meta.url), "utf8");
|
||||
const contractSource = await readFile(new URL("../react/src/xyzac-panel-contract.ts", import.meta.url), "utf8");
|
||||
const sessionSource = await readFile(new URL("./sim-session.mjs", import.meta.url), "utf8");
|
||||
const tpSource = await readFile(new URL("./wasm-linuxcnc-tp.mjs", import.meta.url), "utf8");
|
||||
const combined = `${appSource}\n${panelSource}\n${contractSource}`;
|
||||
for (const symbol of ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacDiagnostics", "KINEMATICS_SELECT", "VISUAL_CLEAR", "POWER", "switchkinsXyzac", "Operation trace", "Alarms / recovery"]) assert.ok(combined.includes(symbol), symbol);
|
||||
for (const symbol of ["wasmSnapshotReady", "manualReady", "sourceJogScale", "sourceVelocityScale", "spindleOverrideSource", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "WASM PENDING", "LinuxCNC WASM override-limits command unavailable"]) assert.ok(appSource.includes(symbol), symbol);
|
||||
for (const forbidden of [/defaultValue=\"50\"/, /defaultValue=\"100\"/, /setBlockDelete/, /setOptionalStop/, /state\.status === \"running\" \? \"READING\"/]) assert.equal(forbidden.test(appSource), false, String(forbidden));
|
||||
for (const forbidden of [/_sim_/, /_malloc/, /\bHEAP(?:U|F|I)?\d*\b/, /navigator\.storage/, /\bOPFS\b/i, /postMessage\s*\(/]) assert.equal(forbidden.test(combined), false, String(forbidden));
|
||||
assert.ok(appSource.includes("SELECTED_XYZAC_CASE"));
|
||||
assert.equal(appSource.includes("3axis-tutorial"), false);
|
||||
assert.ok(sessionSource.includes("this.wasmMotion.jog("));
|
||||
assert.equal(/const\s+target\s*=\s*Number\(start\[axis\]/.test(sessionSource), false);
|
||||
assert.equal(/wasmMotion\.enqueue\(\{\s*start,\s*end/.test(sessionSource), false);
|
||||
assert.equal(tpSource.includes("cubicInterpolate"), false);
|
||||
assert.ok(tpSource.includes("sim_linuxcnc_tp_jog"));
|
||||
|
||||
console.log(JSON.stringify({ schema: "xyzac-protocol-react-rtcp-09.v1", status: "verified", case: CASE, commands: [power.payload.command, "KINEMATICS_SELECT", "VISUAL_CLEAR"], switchkins: switches, gates: { invalid: invalid.payload.gate_reason, running: runningSwitch.payload.gate_reason, controller: controller.exportTrace().map((item) => item.phase) }, components: ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacDiagnostics"], worker_only: true, state_hash: clear.payload.panel_snapshot.state_hash }));
|
||||
|
||||
@@ -5,11 +5,11 @@ const KIN_TYPE_TRIVKINS = 1;
|
||||
const KIN_TYPE_XYZAC_TRT = 2;
|
||||
const KIN_CONFIG_SIZE = 1608;
|
||||
const KIN_SIZE = 1760;
|
||||
const HOME_LIMIT_SIZE = 184;
|
||||
const HOME_STATE_SIZE = 176;
|
||||
const HOME_LIMIT_SIZE = 192;
|
||||
const HOME_STATE_SIZE = 184;
|
||||
const HOME_STATE_OFFSET = 8;
|
||||
const CONFIG_OFFSETS = Object.freeze({ type: 0, mode: 4, joint_count: 8, kinematics_joint_count: 12, extra_joint_count: 16, coordinates: 20, x_rot_point: 40, y_offset: 72, z_offset: 80, tool_offset: 88, limit_enabled: 1448, joint_min: 1464, joint_max: 1536 });
|
||||
const HOME_STATE_OFFSETS = Object.freeze({ pose: 0, joints: 72, joint_count: 144, homed: 148, home_sequence: 152, tick: 156, last_error: 160, limit_error: 164, kinematics_error: 168 });
|
||||
const HOME_STATE_OFFSETS = Object.freeze({ pose: 0, joints: 72, joint_count: 144, homed: 148, home_sequence: 152, tick: 156, last_error: 160, limit_error: 164, kinematics_error: 168, homed_mask: 172 });
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const direct = core?.[`_${name}`] ?? core?.[name];
|
||||
@@ -43,7 +43,7 @@ function finitePose(value) {
|
||||
export class WasmHomeLimit {
|
||||
constructor(core, { jointCount = 3, limits = {}, kinematicsType = KIN_TYPE_TRIVKINS, coordinates = "XYZ", offsets = {} } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_home_limit_init", "sim_kinematics_home_limit_reset", "sim_kinematics_home_limit_home", "sim_kinematics_home_limit_check", "sim_kinematics_home_limit_read"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_home_limit_init", "sim_kinematics_home_limit_reset", "sim_kinematics_home_limit_home", "sim_kinematics_home_limit_home_joint", "sim_kinematics_home_limit_check", "sim_kinematics_home_limit_read"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.jointCount = jointCount;
|
||||
this.kinematicsType = kinematicsType;
|
||||
this.coordinates = String(coordinates).toUpperCase();
|
||||
@@ -93,12 +93,16 @@ export class WasmHomeLimit {
|
||||
const ints = this.core.HEAPU32;
|
||||
const signed = this.core.HEAP32;
|
||||
const joints = [...this.core.HEAPF64.subarray(this.stateBuffer / 8 + 9, this.stateBuffer / 8 + 9 + this.jointCount)];
|
||||
return { status: "verified", state: { schema: HOME_LIMIT_SCHEMA, source: "wasm_home_limit", pose: pose(this.core, this.stateBuffer), joints, joint_count: ints[this.stateBuffer / 4 + 36], homed: Boolean(ints[this.stateBuffer / 4 + 37]), home_sequence: ints[this.stateBuffer / 4 + 38], tick: ints[this.stateBuffer / 4 + 39], last_error: signed[this.stateBuffer / 4 + 40], limit_error: signed[this.stateBuffer / 4 + 41], kinematics_error: signed[this.stateBuffer / 4 + 42] } };
|
||||
return { status: "verified", state: { schema: HOME_LIMIT_SCHEMA, source: "wasm_home_limit", pose: pose(this.core, this.stateBuffer), joints, joint_count: ints[this.stateBuffer / 4 + 36], homed: Boolean(ints[this.stateBuffer / 4 + 37]), home_sequence: ints[this.stateBuffer / 4 + 38], tick: ints[this.stateBuffer / 4 + 39], last_error: signed[this.stateBuffer / 4 + 40], limit_error: signed[this.stateBuffer / 4 + 41], kinematics_error: signed[this.stateBuffer / 4 + 42], homed_mask: ints[this.stateBuffer / 4 + 43] } };
|
||||
}
|
||||
|
||||
home(sequence = 0) {
|
||||
home(sequence = 0, axis = null) {
|
||||
if (!this.available) return this.read();
|
||||
const result = call(this.core, "sim_kinematics_home_limit_home", "number", ["number", "number"], [this.homeLimit, sequence]);
|
||||
const index = axis === null || axis === undefined ? -1 : [...this.coordinates].findIndex((value) => value.toLowerCase() === String(axis).toLowerCase());
|
||||
if (axis !== null && index < 0) return { status: "error", code: "wasm_home_axis_invalid", state: this.read().state };
|
||||
const result = index < 0
|
||||
? call(this.core, "sim_kinematics_home_limit_home", "number", ["number", "number"], [this.homeLimit, sequence])
|
||||
: call(this.core, "sim_kinematics_home_limit_home_joint", "number", ["number", "number", "number"], [this.homeLimit, sequence, index]);
|
||||
if (result !== 0) return { status: "error", code: `wasm_home_${result}`, state: this.read().state };
|
||||
return this.read();
|
||||
}
|
||||
|
||||
@@ -14,32 +14,10 @@ function call(core, name, returnType, argTypes, args) {
|
||||
function poseArray(value = {}) { return ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => Number(value[axis] ?? 0)); }
|
||||
function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis, index) => [axis, Number(values[index] ?? 0)])); }
|
||||
|
||||
function cubicState() { return { x0: 0, x1: 0, x2: 0, x3: 0, t: 0, need: true, coeff: [0, 0, 0, 0] }; }
|
||||
function cubicAdd(state, point, segmentTime = 0.001) {
|
||||
if (!state.need) return;
|
||||
state.x0 = state.x1; state.x1 = state.x2; state.x2 = state.x3; state.x3 = Number(point);
|
||||
const wp0 = (state.x0 + 4 * state.x1 + state.x2) / 6;
|
||||
const wp1 = (state.x1 + 4 * state.x2 + state.x3) / 6;
|
||||
const vel0 = (state.x2 - state.x0) / (2 * segmentTime);
|
||||
const vel1 = (state.x3 - state.x1) / (2 * segmentTime);
|
||||
const b = 3 * (wp1 - wp0) / (segmentTime * segmentTime) - (2 * vel0 + vel1) / segmentTime;
|
||||
const a = (vel1 - vel0) / (3 * segmentTime * segmentTime) - 2 * b / (3 * segmentTime);
|
||||
state.coeff = [a, b, vel0, wp0]; state.t = 0; state.need = false;
|
||||
}
|
||||
function cubicInterpolate(state, segmentTime = 0.001) {
|
||||
if (state.need) cubicAdd(state, state.x3, segmentTime);
|
||||
const [a, b, c, d] = state.coeff;
|
||||
const t = state.t;
|
||||
const value = ((a * t + b) * t + c) * t + d;
|
||||
state.t += segmentTime;
|
||||
if (Math.abs(segmentTime - state.t) < segmentTime * 0.5) state.need = true;
|
||||
return value;
|
||||
}
|
||||
|
||||
export class WasmLinuxcncTp {
|
||||
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300 } = {}) {
|
||||
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.cycleTime = cycleTime;
|
||||
this.velocityLimit = velocityLimit;
|
||||
this.accelerationLimit = accelerationLimit;
|
||||
@@ -49,7 +27,6 @@ export class WasmLinuxcncTp {
|
||||
this.centerPointer = 0;
|
||||
this.normalPointer = 0;
|
||||
this.statePointer = 0;
|
||||
this.history = [];
|
||||
this.activeTag = 0;
|
||||
this.segmentTags = new Map();
|
||||
this.lastActiveId = 0;
|
||||
@@ -60,9 +37,7 @@ export class WasmLinuxcncTp {
|
||||
this.lastState = null;
|
||||
this.queueDepthOverride = null;
|
||||
this.kinematics = null;
|
||||
this.cubic = Array.from({ length: 5 }, () => cubicState());
|
||||
this.previousJointCommanded = [0, 0, 0, 0, 0];
|
||||
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, paused: false };
|
||||
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, velocity_scale: 1, jog_scale: 1, paused: false };
|
||||
if (!this.available || typeof core?._malloc !== "function") return;
|
||||
this.planner = core._malloc(call(core, "sim_linuxcnc_tp_size", "number", [], []));
|
||||
this.posePointer = core._malloc(POSE_BYTES);
|
||||
@@ -74,6 +49,10 @@ export class WasmLinuxcncTp {
|
||||
if (result !== 0) throw new Error(`linuxcnc_tp_init_${result}`);
|
||||
this.setPosition({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
|
||||
this.setProfile(0);
|
||||
for (const [axis, limit] of Object.entries(limits)) {
|
||||
const index = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5, u: 6, v: 7, w: 8 }[axis];
|
||||
if (index !== undefined && Array.isArray(limit) && limit.length === 2) this.setAxisLimit(index, Number(limit[0]), Number(limit[1]));
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(value) {
|
||||
@@ -111,6 +90,11 @@ export class WasmLinuxcncTp {
|
||||
setLookahead(value) { if (!Number.isSafeInteger(value) || value < 1) throw new Error("linuxcnc_tp_lookahead_invalid"); this.controls.lookahead = value; return this.read(); }
|
||||
setDynamics(accelerationLimit, jerkLimit) { this.accelerationLimit = Number(accelerationLimit); this.controls.acceleration_limit = this.accelerationLimit; this.controls.jerk_limit = Number(jerkLimit); if (this.available) call(this.core, "sim_linuxcnc_tp_set_dynamics", "number", ["number", "number"], [this.planner, this.accelerationLimit]); return this.read(); }
|
||||
setFeedOverride(value) { this.controls.feed_override = Number(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_feed_override", "number", ["number", "number"], [this.planner, this.controls.feed_override]); return this.read(); }
|
||||
setVelocityScale(value) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_velocity_scale", "number", ["number", "number"], [this.planner, Number(value)]); if (result !== 0) throw new Error(`linuxcnc_tp_velocity_scale_${result}`); return this.read(); }
|
||||
setJogScale(value) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_jog_scale", "number", ["number", "number"], [this.planner, Number(value)]); if (result !== 0) throw new Error(`linuxcnc_tp_jog_scale_${result}`); return this.read(); }
|
||||
setAxisLimit(axis, minimum, maximum) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_axis_limit", "number", ["number", "number", "number", "number"], [this.planner, Number(axis), Number(minimum), Number(maximum)]); if (result !== 0) throw new Error(`linuxcnc_tp_axis_limit_${result}`); return this.read(); }
|
||||
jog(axis, direction, increment, tag = 0) { if (!this.available) return { status: "blocked", code: "linuxcnc_tp_unavailable" }; const result = call(this.core, "sim_linuxcnc_tp_jog", "number", ["number", "number", "number", "number", "number"], [this.planner, Number(axis), Number(direction), Number(increment), Number(tag)]); return result === 0 ? { status: "verified", command: "JOG_START" } : { status: "error", code: `linuxcnc_tp_jog_${result}` }; }
|
||||
jogVelocity(axis) { if (!this.available) return 0; const index = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5, u: 6, v: 7, w: 8 }[String(axis).toLowerCase()]; return index === undefined ? 0 : Number(call(this.core, "sim_linuxcnc_tp_jog_velocity", "number", ["number", "number"], [this.planner, index])); }
|
||||
setPaused(value) { this.controls.paused = Boolean(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_paused", "number", ["number", "number"], [this.planner, this.controls.paused ? 1 : 0]); return this.read(); }
|
||||
|
||||
enqueue(segment) {
|
||||
@@ -158,7 +142,7 @@ export class WasmLinuxcncTp {
|
||||
if (Number(activeTag) === 0) return [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
|
||||
const inverse = this.kinematics?.inverseMode?.(Number(activeTag), position) ?? this.kinematics?.inverse?.(position);
|
||||
if (inverse?.status === "verified" && Array.isArray(inverse.joints)) return inverse.joints.slice(0, 5);
|
||||
return this.previousJointCommanded.slice();
|
||||
throw new Error(`linuxcnc_tp_joint_inverse_${inverse?.code ?? "unavailable"}`);
|
||||
}
|
||||
|
||||
step() {
|
||||
@@ -169,13 +153,9 @@ export class WasmLinuxcncTp {
|
||||
const state = this.read();
|
||||
if (state.status === "verified") {
|
||||
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
|
||||
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
|
||||
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
|
||||
state.state.joint_commanded = commanded;
|
||||
state.state.joint_actual = this.previousJointCommanded.slice();
|
||||
this.previousJointCommanded = commanded.slice();
|
||||
state.state.joint_commanded = targetJoints;
|
||||
state.state.joint_actual = targetJoints.slice();
|
||||
}
|
||||
if (state.status === "verified") this.history.push(structuredClone(state.state));
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -186,18 +166,12 @@ export class WasmLinuxcncTp {
|
||||
const state = this.read();
|
||||
if (state.status === "verified") {
|
||||
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
|
||||
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
|
||||
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
|
||||
state.state.joint_commanded = commanded;
|
||||
state.state.joint_actual = this.previousJointCommanded.slice();
|
||||
this.previousJointCommanded = commanded.slice();
|
||||
state.state.joint_commanded = targetJoints;
|
||||
state.state.joint_actual = targetJoints.slice();
|
||||
}
|
||||
if (state.status === "verified") this.history.push(structuredClone(state.state));
|
||||
return state;
|
||||
}
|
||||
|
||||
drainTrace() { const trace = this.history; this.history = []; return trace; }
|
||||
|
||||
read() {
|
||||
if (!this.available) return { status: "blocked", state: { position: toPose([]), velocity: 0, progress: 0, tick: 0, queue_depth: 0, active_id: 0, active_tag: 0, motion_type: 0, status: -1214, exec_state: this.executionState, source: "linuxcnc-source-tp" } };
|
||||
const result = call(this.core, "sim_linuxcnc_tp_read_state", "number", ["number", "number"], [this.planner, this.statePointer]);
|
||||
@@ -207,13 +181,22 @@ export class WasmLinuxcncTp {
|
||||
const activeId = ints[2];
|
||||
if (activeId && activeId !== this.lastActiveId && this.segmentTags.has(activeId)) this.activeTag = this.segmentTags.get(activeId);
|
||||
this.lastActiveId = activeId;
|
||||
const state = { position: toPose(doubles), velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: this.previousJointCommanded.slice(), joint_actual: this.previousJointCommanded.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, source: "linuxcnc-source-tp" };
|
||||
const velocityScale = Number(call(this.core, "sim_linuxcnc_tp_velocity_scale", "number", ["number"], [this.planner]));
|
||||
const velocityLimit = Number(call(this.core, "sim_linuxcnc_tp_velocity_limit", "number", ["number"], [this.planner]));
|
||||
const jogScale = Number(call(this.core, "sim_linuxcnc_tp_jog_scale", "number", ["number"], [this.planner]));
|
||||
const feedOverride = Number(call(this.core, "sim_linuxcnc_tp_feed_override", "number", ["number"], [this.planner]));
|
||||
this.controls.velocity_scale = velocityScale;
|
||||
this.controls.jog_scale = jogScale;
|
||||
this.controls.feed_override = feedOverride;
|
||||
const position = toPose(doubles);
|
||||
const joints = this.commandedJoints(position, this.activeTag);
|
||||
const state = { position, velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity("a"), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" };
|
||||
this.lastState = state;
|
||||
return { status: "verified", state };
|
||||
}
|
||||
|
||||
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; this.cubic = Array.from({ length: 5 }, () => cubicState()); this.previousJointCommanded = [0, 0, 0, 0, 0]; return this.read(); }
|
||||
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
|
||||
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
|
||||
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
|
||||
exportState() {
|
||||
const current = this.read();
|
||||
const segments = [];
|
||||
@@ -241,6 +224,8 @@ export class WasmLinuxcncTp {
|
||||
this.executionState = Number(value.execution_state ?? 3);
|
||||
this.setProfile(value.controls?.profile ?? 0, value.controls?.tolerance ?? 0);
|
||||
this.setFeedOverride(value.controls?.feed_override ?? 1);
|
||||
this.setVelocityScale(value.controls?.velocity_scale ?? 1);
|
||||
this.setJogScale(value.controls?.jog_scale ?? 1);
|
||||
this.setPosition(value.state?.position ?? {});
|
||||
for (const segment of value.segments ?? []) this.enqueue({ ...segment, kinematics_mode: segment.kinematics_mode ?? value.active_tag ?? 0 });
|
||||
if (this.available && value.state) {
|
||||
|
||||
@@ -141,9 +141,12 @@ export class WasmXyzacIo {
|
||||
if (kind === "RESET") return this.invoke("sim_io_reset");
|
||||
if (kind === "SPINDLE_COMMAND") {
|
||||
if (payload.delta !== undefined) return this.invoke("sim_io_spindle_adjust", [payload.spindle ?? 0, Number(payload.delta)]);
|
||||
return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, payload.rpm ?? payload.speed ?? 0, payload.enabled === false ? 0 : 1]);
|
||||
const requestedRpm = Number(payload.rpm ?? payload.speed ?? 0);
|
||||
const signedRpm = payload.direction === -1 ? -requestedRpm : payload.direction === 1 ? requestedRpm : requestedRpm;
|
||||
return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, signedRpm, payload.enabled === false ? 0 : 1]);
|
||||
}
|
||||
if (kind === "SPINDLE_BRAKE") return this.invoke("sim_io_spindle_brake", [payload.spindle ?? 0, payload.engaged === true ? 1 : 0]);
|
||||
if (kind === "SPINDLE_OVERRIDE" || kind === "SET_SPINDLE_OVERRIDE") return this.invoke("sim_io_spindle_set_override", [payload.spindle ?? 0, Number(payload.scale ?? payload.override ?? 1)]);
|
||||
if (kind === "SPINDLE_FAULT") return this.invoke("sim_io_spindle_set_fault", [payload.spindle ?? 0, payload.fault === true ? 1 : 0]);
|
||||
if (kind === "SPINDLE_INHIBIT") return this.invoke("sim_io_spindle_set_inhibit", [payload.spindle ?? 0, payload.inhibit === true ? 1 : 0]);
|
||||
if (kind === "SPINDLE_ORIENT") return this.invoke("sim_io_spindle_orient", [payload.spindle ?? 0, payload.angle ?? 0, payload.mode ?? 0]);
|
||||
|
||||
@@ -46,11 +46,18 @@ export class XyzacCommandLedger extends OperationLedger {
|
||||
}
|
||||
|
||||
audit({ includeResponses = true, maxEntries = Number.POSITIVE_INFINITY } = {}) {
|
||||
const exported = super.export();
|
||||
// High-frequency SIM_STEP responses request only a small audit window.
|
||||
// OperationLedger.export() clones every historical response before the
|
||||
// window can be selected, which turns a long run into quadratic browser
|
||||
// serialization work. The map is insertion ordered by command sequence,
|
||||
// so select the tail first and clone only the returned entries.
|
||||
const values = [...this.entries.values()];
|
||||
const pending = this.pending();
|
||||
const totalEntries = exported.entries.length;
|
||||
const windowed = Number.isFinite(maxEntries) && maxEntries >= 0 ? exported.entries.slice(-Math.floor(maxEntries)) : exported.entries;
|
||||
const entries = includeResponses ? windowed : windowed.map((entry) => { const { response, ...summary } = entry; return summary; });
|
||||
const totalEntries = values.length;
|
||||
const windowed = Number.isFinite(maxEntries) && maxEntries >= 0 ? values.slice(-Math.floor(maxEntries)) : values;
|
||||
const entries = includeResponses
|
||||
? windowed.map((entry) => structuredClone(entry))
|
||||
: windowed.map((entry) => { const { response, ...summary } = entry; return structuredClone(summary); });
|
||||
const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries, entry_count: totalEntries, truncated: entries.length < totalEntries, pending_count: pending.length };
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user