Align Web XYZAC command gates with LinuxCNC
This commit is contained in:
@@ -45,7 +45,7 @@ function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programR
|
||||
<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>
|
||||
<TextButton disabled={isEstop || programRunning || !canPower} onClick={onPower} ariaLabel={machineOn ? "Power off" : "Power on"}>{machineOn ? "Power Off" : "Power On"}</TextButton>
|
||||
<TextButton disabled={!canPower} onClick={onPower} ariaLabel={machineOn ? "Power off" : "Power on"}>{machineOn ? "Power Off" : "Power On"}</TextButton>
|
||||
<TextButton disabled={!canHome} onClick={onHomeAll} ariaLabel="Home all axes">HOME ALL</TextButton>
|
||||
<TextButton disabled={!canRun} onClick={onRun} ariaLabel="Run program">Run Program</TextButton>
|
||||
<TextButton feature="AXIS-MACHINE-SEQUENCE" disabled={!programLoaded || programRunning} onClick={onSequence} ariaLabel="Run startup sequence">Run Startup Sequence</TextButton>
|
||||
@@ -172,7 +172,7 @@ export function App() {
|
||||
const emergencyStop = () => { stopRunTimer(); stopJogTimer(); void dispatch("ESTOP", { active: true }, false).catch(() => undefined); };
|
||||
const releaseEstop = () => dispatch("ESTOP", { active: false }, false);
|
||||
const toggleEstop = () => { if (!isEstop) { emergencyStop(); return; } void releaseEstop().catch(() => undefined); };
|
||||
const powerOn = () => { if (isEstop || programRunning || !gates.power.enabled) return; void dispatch("POWER", { active: !machineOn }, false).catch(() => undefined); };
|
||||
const powerOn = () => { if (!gates.power.enabled) return; void dispatch("POWER", { active: isEstop ? false : !machineOn }, false).catch(() => undefined); };
|
||||
const resetMachine = () => { stopRunTimer(); stopJogTimer(); if (isEstop || programRunning || !machineOn) return; void dispatch("RESET").catch(() => undefined); };
|
||||
const homeAll = () => { void dispatch("HOME", { all: true }).catch(() => undefined); };
|
||||
const runStartupSequence = () => {
|
||||
@@ -225,8 +225,8 @@ export function App() {
|
||||
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")}>
|
||||
<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={openProgramFile} disabled={programRunning}>Open G-code...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)} disabled={programRunning}>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={isEstop || programRunning || !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}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled}>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}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning}>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={isEstop || programRunning || !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" 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); 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>
|
||||
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={openProgramFile} disabled={programRunning}>Open G-code...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)} disabled={programRunning}>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}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled}>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}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning}>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" 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); 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-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}>Load case</TextButton><span className="case-status" role="status">{caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></section>
|
||||
<MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} 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} />
|
||||
|
||||
@@ -56,6 +56,21 @@ const PANEL_GATE_ACTIONS = Object.freeze({
|
||||
POWER: "power",
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const WASM_GATE_REASONS = Object.freeze({
|
||||
1: "xyzac_estop_active", 2: "xyzac_machine_off", 3: "xyzac_power_already_on",
|
||||
4: "xyzac_interp_running", 5: "axis_taskfile_missing", 6: "linuxcnc_wasm_estop_inactive",
|
||||
7: "xyzac_pause_state_gate", 8: "xyzac_resume_state_gate", 9: "xyzac_abort_state_gate",
|
||||
10: "xyzac_restart_state_gate", 11: "xyzac_kinematics_type_invalid", 12: "linuxcnc_wasm_mode_unavailable",
|
||||
13: "linuxcnc_wasm_auto_interp_active", 14: "xyzac_jog_state_gate", 15: "xyzac_mdi_queue_full",
|
||||
16: "xyzac_reset_state_gate", 17: "linuxcnc_wasm_gate_unknown_command", 18: "xyzac_not_all_homed",
|
||||
});
|
||||
|
||||
function text(value) {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
@@ -216,6 +231,21 @@ export class SimSession {
|
||||
return { status: "verified", count: rows.length };
|
||||
}
|
||||
|
||||
resetXyzacProgramToLoaded() {
|
||||
if (!this.xyzacProgram?.source) return { status: "verified" };
|
||||
const source = this.xyzacProgram.source;
|
||||
const path = this.xyzacProgram.path;
|
||||
const loaded = this.xyzacProgram.load(source, path);
|
||||
if (loaded.status !== "verified") return loaded;
|
||||
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
const subroutines = this.loadXyzacSubroutines(path);
|
||||
if (subroutines.status !== "verified") return subroutines;
|
||||
const preview = this.xyzacProgram.preview();
|
||||
if (preview.status !== "verified") return preview;
|
||||
this.previewState = preview;
|
||||
return { status: "verified" };
|
||||
}
|
||||
|
||||
panelSnapshot({ task = this.task.export(), program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : null, input = {}, tick = this.tick, motionTrace = null, includeToolpath = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeConfiguration = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeStableRuntime = input.kind !== "SIM_STEP" || !this.xyzacProgram } = {}) {
|
||||
const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
@@ -247,7 +277,7 @@ export class SimSession {
|
||||
home: this.xyzacCommandGate({ kind: "HOME", payload: {} }),
|
||||
estop: this.xyzacCommandGate({ kind: "ESTOP", payload: { active: true } }),
|
||||
recover: this.xyzacCommandGate({ kind: "IO_RECOVER", payload: {} }),
|
||||
power: this.xyzacCommandGate({ kind: "POWER", payload: { active: !Boolean(ioState.ready) } }),
|
||||
power: this.xyzacCommandGate({ kind: "POWER", payload: { active: ioState.estop ? false : !Boolean(ioState.ready) } }),
|
||||
powerOn: this.xyzacCommandGate({ kind: "POWER", payload: { active: true } }),
|
||||
powerOff: this.xyzacCommandGate({ kind: "POWER", payload: { active: false } }),
|
||||
reset: this.xyzacCommandGate({ kind: "RESET", payload: {} }),
|
||||
@@ -360,64 +390,47 @@ export class SimSession {
|
||||
const program = this.xyzacProgram.snapshot();
|
||||
const io = this.xyzacIo?.canonical?.() ?? {};
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
const active = task.state === "RUNNING" || task.state === "PAUSED" || program.status === 2 || program.status === 3;
|
||||
const homeLimit = this.wasmHomeLimit.read();
|
||||
const estop = Boolean(io.estop);
|
||||
const ready = Boolean(io.ready) && !estop;
|
||||
const loaded = program.status !== 0;
|
||||
const action = PANEL_GATE_ACTIONS[input.kind] ?? null;
|
||||
let enabled = true;
|
||||
let reason = "linuxcnc_wasm_authority";
|
||||
if (input.kind === "KINEMATICS_SELECT" && ![0, 1, 2].includes(Number(input.payload?.type))) {
|
||||
enabled = false;
|
||||
reason = "xyzac_kinematics_type_invalid";
|
||||
} else if (input.kind === "ESTOP") {
|
||||
enabled = input.payload?.active === true || estop;
|
||||
reason = enabled ? "linuxcnc_wasm_estop_transition" : "linuxcnc_wasm_estop_inactive";
|
||||
} else if (input.kind === "POWER") {
|
||||
const requested = input.payload?.active === true;
|
||||
enabled = requested ? !estop && !ready && !active : !estop && ready && !active;
|
||||
reason = estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : enabled ? "linuxcnc_wasm_power_transition" : requested ? "xyzac_power_already_on" : "xyzac_power_already_off";
|
||||
} else if (input.kind === "IO_RECOVER") {
|
||||
enabled = !estop && ready && !active;
|
||||
reason = enabled ? "linuxcnc_wasm_io_recover" : estop ? "xyzac_estop_active" : !ready ? "xyzac_power_required" : "xyzac_interp_running";
|
||||
} else if (input.kind === "LOAD_WORKSPACE") {
|
||||
enabled = !estop && !active;
|
||||
reason = enabled ? "linuxcnc_wasm_workspace_available" : estop ? "xyzac_estop_active" : "xyzac_interp_running";
|
||||
} else if (["PROGRAM_LOAD", "PROGRAM_RESTART"].includes(input.kind)) {
|
||||
enabled = !estop && !active && (input.kind !== "PROGRAM_RESTART" || loaded);
|
||||
reason = estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : enabled ? "linuxcnc_wasm_program_idle" : "xyzac_taskfile_missing";
|
||||
} else if (input.kind === "PROGRAM_RUN") {
|
||||
enabled = ready && !active && loaded;
|
||||
reason = !ready ? (estop ? "xyzac_estop_active" : "xyzac_machine_off") : active ? "xyzac_interp_running" : loaded ? "linuxcnc_wasm_run_available" : "axis_taskfile_missing";
|
||||
} else if (input.kind === "PROGRAM_PAUSE") {
|
||||
enabled = ready && program.status === 2;
|
||||
reason = enabled ? "linuxcnc_wasm_pause_available" : "xyzac_pause_state_gate";
|
||||
} else if (input.kind === "PROGRAM_RESUME") {
|
||||
enabled = ready && program.status === 3;
|
||||
reason = enabled ? "linuxcnc_wasm_resume_available" : "xyzac_resume_state_gate";
|
||||
} else if (input.kind === "PROGRAM_ABORT") {
|
||||
enabled = active;
|
||||
reason = enabled ? "linuxcnc_wasm_abort_available" : "xyzac_abort_state_gate";
|
||||
} else if (input.kind === "PROGRAM_STEP") {
|
||||
enabled = ready && loaded && !active;
|
||||
reason = !ready ? "xyzac_machine_off" : loaded ? "linuxcnc_wasm_step_available" : "axis_taskfile_missing";
|
||||
} else if (input.kind === "SET_MODE") {
|
||||
enabled = ready && !active;
|
||||
reason = enabled ? "linuxcnc_wasm_mode_available" : estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : "xyzac_machine_off";
|
||||
} else if (input.kind === "RESET") {
|
||||
enabled = ready && !active;
|
||||
reason = enabled ? "linuxcnc_wasm_reset_available" : estop ? "xyzac_estop_active" : !ready ? "xyzac_machine_off" : "xyzac_interp_running";
|
||||
} else if (input.kind === "HOME" || ["JOG_START", "JOG_STOP", "SET_OVERRIDE"].includes(input.kind)) {
|
||||
enabled = ready && !active;
|
||||
reason = enabled ? "linuxcnc_wasm_manual_available" : estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : "xyzac_machine_off";
|
||||
} else if (input.kind === "MDI_EXECUTE") {
|
||||
enabled = ready && !active;
|
||||
reason = enabled ? "linuxcnc_wasm_mdi_available" : estop ? "xyzac_estop_active" : active ? "mdi_while_program_active" : "xyzac_machine_off";
|
||||
} else if (["KINEMATICS_SELECT", "SPINDLE_COMMAND", "SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "TOOL_PREPARE", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_CHANGED", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT", "PROBE_CONFIG"].includes(input.kind)) {
|
||||
enabled = ready && !active;
|
||||
reason = enabled ? "linuxcnc_wasm_io_available" : estop ? "xyzac_estop_active" : active ? "xyzac_interp_running" : "xyzac_machine_off";
|
||||
}
|
||||
return { action, enabled, reason, taskState: task.state, interpState: active ? (program.status === 3 ? "PAUSED" : "READING") : "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status, queue_depth: wasmMotion.state.queue_depth } };
|
||||
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" || input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.JOG
|
||||
: 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
|
||||
: input.kind === "PROGRAM_PAUSE" ? WASM_GATE_COMMANDS.PAUSE
|
||||
: input.kind === "PROGRAM_RESUME" ? WASM_GATE_COMMANDS.RESUME
|
||||
: input.kind === "PROGRAM_ABORT" ? WASM_GATE_COMMANDS.ABORT
|
||||
: input.kind === "PROGRAM_STEP" ? WASM_GATE_COMMANDS.STEP
|
||||
: input.kind === "MDI_EXECUTE" ? WASM_GATE_COMMANDS.MDI
|
||||
: input.kind === "KINEMATICS_SELECT" ? WASM_GATE_COMMANDS.KINEMATICS
|
||||
: input.kind === "SET_MODE" ? WASM_GATE_COMMANDS.SET_MODE
|
||||
: input.kind === "RESET" ? WASM_GATE_COMMANDS.RESET
|
||||
: input.kind === "HOME" ? WASM_GATE_COMMANDS.HOME
|
||||
: input.kind === "ESTOP" ? WASM_GATE_COMMANDS.ESTOP
|
||||
: input.kind === "POWER" ? WASM_GATE_COMMANDS.POWER
|
||||
: input.kind === "IO_RECOVER" ? WASM_GATE_COMMANDS.POWER : null;
|
||||
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 === "SET_MODE" ? ({ MANUAL: 0, MDI: 1, AUTO: 2 }[String(input.payload?.mode ?? "").toUpperCase()] ?? 3) : 0;
|
||||
const result = this.task.commandGate(command, {
|
||||
requested,
|
||||
ioReady: ready,
|
||||
ioEstop: estop,
|
||||
homed: Boolean(homeLimit.state?.homed),
|
||||
queueDepth: Number(task.queued_mdi_commands ?? 0),
|
||||
queueLimit: Number(task.max_queued_mdi_commands ?? 10),
|
||||
});
|
||||
if (result === null) return { action, enabled: false, reason: "linuxcnc_wasm_gate_unavailable", taskState: task.state, interpState: "UNKNOWN" };
|
||||
let reason = result === 0 ? "linuxcnc_wasm_authority" : WASM_GATE_REASONS[result] ?? "linuxcnc_wasm_gate_unknown_command";
|
||||
if (result === 2 && input.kind === "POWER" && input.payload?.active !== true) reason = "xyzac_power_already_off";
|
||||
if (result === 3 && input.kind === "POWER") reason = "xyzac_power_already_on";
|
||||
if (result === 4 && input.kind === "MDI_EXECUTE") reason = "mdi_while_program_active";
|
||||
if (result === 4 && input.kind === "PROGRAM_STEP") reason = "xyzac_interp_running";
|
||||
if (result === 5 && input.kind === "PROGRAM_RESTART") reason = "xyzac_taskfile_missing";
|
||||
return { action, enabled: result === 0, reason, taskState: task.state, interpState: task.state === "PAUSED" || program.status === 3 ? "PAUSED" : task.state === "RUNNING" || program.status === 2 ? "READING" : "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status, queue_depth: wasmMotion.state.queue_depth, gate_result: result } };
|
||||
}
|
||||
|
||||
stateForCheckpoint() {
|
||||
@@ -971,13 +984,15 @@ export class SimSession {
|
||||
const deviceResponse = (kind, payload) => {
|
||||
const result = this.xyzacIo ? this.xyzacIo.command(kind, payload ?? {}) : this.devices.command(kind, payload ?? {});
|
||||
if (result.status === "verified" && this.xyzacProgram && kind === "ESTOP" && payload?.active === true) {
|
||||
// The WASM IO edge owns emergency stop. Stop the WASM trajectory and
|
||||
// pause the canonical program so no compatibility state can continue
|
||||
// feeding motion while the machine is stopped.
|
||||
const programState = this.xyzacProgram.snapshot();
|
||||
if (programState.status === 2) this.xyzacProgram.pause();
|
||||
// LinuxCNC emcTaskSetState(ESTOP) calls emcTaskAbort: clear the
|
||||
// interpreter command and trajectory, then leave the taskfile loaded
|
||||
// for a later AUTO run after ESTOP_RESET and POWER ON.
|
||||
this.xyzacProgram.abort();
|
||||
this.wasmMotion.abort();
|
||||
this.wasmMotion.setPaused(true);
|
||||
const resetProgram = this.resetXyzacProgramToLoaded();
|
||||
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
||||
this.task.setProgram("loaded", 0);
|
||||
}
|
||||
try {
|
||||
if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904);
|
||||
@@ -1053,10 +1068,20 @@ export class SimSession {
|
||||
if (this.xyzacProgram) {
|
||||
const result = this.xyzacIo.command("POWER", input.payload ?? {});
|
||||
if (result.status === "verified") {
|
||||
if (input.payload?.active === false) {
|
||||
/* LinuxCNC emcTaskSetState(OFF) aborts motion, clears the
|
||||
interpreter command and leaves the loaded taskfile intact. */
|
||||
this.xyzacProgram.abort();
|
||||
this.wasmMotion.abort();
|
||||
this.wasmMotion.setPaused(true);
|
||||
const resetProgram = this.resetXyzacProgramToLoaded();
|
||||
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
||||
this.task.setProgram("loaded", 0);
|
||||
}
|
||||
try {
|
||||
if (input.payload?.active === true) this.task.recover();
|
||||
else if (["READY", "STOPPED"].includes(this.task.state.state)) {
|
||||
if (this.task.state.state === "READY") this.task.transition("STOPPED");
|
||||
else if (["READY", "RUNNING", "PAUSED", "STOPPED"].includes(this.task.state.state)) {
|
||||
if (["READY", "RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
|
||||
}
|
||||
} catch { /* the WASM IO state remains authoritative */ }
|
||||
if (this.xyzacDeviceIo) this.xyzacDeviceIo.command("INTERLOCK_RESET", input.payload ?? {}, input.operation_id);
|
||||
@@ -1468,6 +1493,7 @@ export class SimSession {
|
||||
case "PROGRAM_STEP": {
|
||||
if (this.xyzacProgram) {
|
||||
const current = this.xyzacProgram.snapshot();
|
||||
if (current.status === 1 && this.task.state.mode !== "AUTO") this.task.setMode("AUTO");
|
||||
if (current.status === 1) this.xyzacProgram.start();
|
||||
const result = this.xyzacProgram.step();
|
||||
if (result.status === "verified") { if (result.motion) this.wasmMotion.enqueue(result.motion); this.wasmMotion.setPaused(true); this.task.setProgram(result.program_status === 4 ? "completed" : "paused", result.line); this.task.transition(result.program_status === 4 ? "STOPPED" : "PAUSED"); }
|
||||
@@ -1523,6 +1549,7 @@ export class SimSession {
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
const block = input.payload?.block ?? input.payload?.command;
|
||||
if (this.xyzacProgram) {
|
||||
this.task.setMode("MDI");
|
||||
const result = this.xyzacProgram.executeBlock(block);
|
||||
if (result.status === "verified") {
|
||||
const controlCode = Number(result.program?.control_code ?? 0);
|
||||
@@ -1555,6 +1582,7 @@ export class SimSession {
|
||||
if (!this.xyzacKinematics) return finish(this.error(input, "kinematics", "xyzac_kinematics_unavailable"), "failed");
|
||||
const mode = Number(input.payload?.type);
|
||||
if (![XYZAC_MODES.identity, XYZAC_MODES.xyzac, XYZAC_MODES.userk].includes(mode)) return finish(this.error(input, "kinematics", "xyzac_kinematics_type_invalid"), "failed");
|
||||
this.task.setMode("MDI");
|
||||
const result = this.xyzacKinematics.select(mode, this.xyzacKinematics.snapshot().joints);
|
||||
const code = mode === XYZAC_MODES.xyzac ? "M428" : mode === XYZAC_MODES.identity ? "M429" : "M430";
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
|
||||
@@ -170,6 +170,12 @@ export class SimTask {
|
||||
this.lastError = 0;
|
||||
}
|
||||
|
||||
commandGate(command, { requested = 0, ioReady = false, ioEstop = false, homed = false, queueDepth = 0, queueLimit = 10 } = {}) {
|
||||
this.ensureCore();
|
||||
if (!this.coreTask || (typeof this.core?._sim_task_command_gate !== "function" && typeof this.core?.sim_task_command_gate !== "function")) return null;
|
||||
return coreCall(this.core, "sim_task_command_gate", "number", ["number", "number", "number", "number", "number", "number", "number", "number"], [this.coreTask, command, requested, ioReady ? 1 : 0, ioEstop ? 1 : 0, homed ? 1 : 0, queueDepth, queueLimit]);
|
||||
}
|
||||
|
||||
export() {
|
||||
this.syncFromCore();
|
||||
const unsigned = clone(this.state);
|
||||
|
||||
@@ -30,20 +30,22 @@ 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("KINEMATICS_SELECT", { type: 1 }).payload.kinematics.active_type, 1);
|
||||
assert.equal(send("HOME", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
assert.equal(send("KINEMATICS_SELECT", { type: 1 }).payload.kinematics.active_type, 1);
|
||||
assert.equal(send("POWER", { active: false }).payload.panel_snapshot.axis_state.machine_state, "OFF");
|
||||
assert.equal(send("PROGRAM_RUN").payload.gate_reason, "xyzac_machine_off");
|
||||
assert.equal(send("POWER", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ON");
|
||||
|
||||
assert.equal(send("PROGRAM_RUN").payload.task.state, "RUNNING");
|
||||
for (const [kind, payload, reason] of [["HOME", { all: true }, "xyzac_interp_running"], ["MDI_EXECUTE", { block: "G0 X1" }, "mdi_while_program_active"], ["KINEMATICS_SELECT", { type: 0 }, "xyzac_interp_running"], ["POWER", { active: false }, "xyzac_interp_running"]]) {
|
||||
for (const [kind, payload, reason] of [["HOME", { all: true }, "xyzac_interp_running"], ["MDI_EXECUTE", { block: "G0 X1" }, "mdi_while_program_active"], ["KINEMATICS_SELECT", { type: 0 }, "xyzac_interp_running"]]) {
|
||||
const blocked = send(kind, payload);
|
||||
assert.equal(blocked.kind, "ERROR", kind);
|
||||
assert.equal(blocked.payload.gate_reason, reason, kind);
|
||||
}
|
||||
assert.equal(send("PROGRAM_PAUSE").payload.task.state, "PAUSED");
|
||||
assert.equal(send("PROGRAM_RESUME").payload.task.state, "RUNNING");
|
||||
assert.equal(send("POWER", { active: false }).payload.task.state, "STOPPED");
|
||||
assert.equal(send("POWER", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ON");
|
||||
assert.equal(send("PROGRAM_ABORT").payload.task.state, "STOPPED");
|
||||
|
||||
assert.equal(send("ESTOP", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ESTOP");
|
||||
|
||||
@@ -28,6 +28,7 @@ assert.equal(initial.gates.switchkinsXyzac.enabled, false);
|
||||
assert.equal(initial.gates.switchkinsUserk.enabled, false);
|
||||
assert.equal(initial.gates.vismachClear.visual_only, true);
|
||||
assert.equal(send("POWER", "power-on", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ON");
|
||||
assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
const powered = send("SNAPSHOT_REQUEST", "powered-snapshot").payload.panel_snapshot;
|
||||
assert.equal(powered.gates.switchkinsIdentity.enabled, true);
|
||||
assert.equal(powered.gates.switchkinsXyzac.enabled, true);
|
||||
|
||||
@@ -15,6 +15,7 @@ const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kin
|
||||
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);
|
||||
|
||||
const xml = await readFile(new URL("../../linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml", import.meta.url), "utf8");
|
||||
const postgui = await readFile(new URL("../../linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal", import.meta.url), "utf8");
|
||||
|
||||
@@ -17,6 +17,7 @@ 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);
|
||||
return { session, send };
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", conf
|
||||
const power = send("POWER", "power-on", { active: true });
|
||||
assert.equal(power.kind, "COMMAND_RESULT");
|
||||
assert.equal(power.payload.command, "POWER");
|
||||
assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
const switches = [];
|
||||
for (const type of [0, 1, 2, 1]) {
|
||||
const response = send("KINEMATICS_SELECT", `select-${type}-${seq}`, { type });
|
||||
|
||||
@@ -28,6 +28,7 @@ async function create(id) {
|
||||
const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
assert.equal(loaded.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);
|
||||
return { core, session, send };
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ 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);
|
||||
|
||||
const switchRows = [];
|
||||
for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2], ["M428", 1]]) {
|
||||
|
||||
@@ -29,6 +29,7 @@ assert.equal(send("HELLO", "hello").kind, "CAPABILITIES");
|
||||
assert.equal(send("SESSION_ATTACH", "attach").kind, "SESSION_READY");
|
||||
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);
|
||||
assert.equal(session.xyzacKinematics.forward([12, -5, 20, 30, 45]).status, "verified");
|
||||
|
||||
const levels = { protocol: [], control: [], state: [], canonical: [], recovery: [] };
|
||||
|
||||
@@ -35,6 +35,7 @@ assert.equal(loaded.payload.motion_authority, "wasm");
|
||||
assert.equal(loaded.payload.configuration.state, "TASK_READY");
|
||||
assert.equal(loaded.payload.panel_snapshot.kinematics.joint_count, 5);
|
||||
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);
|
||||
|
||||
const switchRows = [];
|
||||
for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2]]) {
|
||||
|
||||
@@ -15,6 +15,7 @@ const send = (kind, operation_id, payload = {}) => session.accept({ ...base, kin
|
||||
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);
|
||||
const source = "G21 G90\nG0 X10 Y5 Z200 A30 C45\nG1 X20 Y10 Z180 A35 C50 F100\nG1 X30 Y15 Z160 A40 C55 F100\nM2\n";
|
||||
const loaded = send("PROGRAM_LOAD", "program-load", { source, path: "workflow.ngc" });
|
||||
assert.equal(loaded.kind, "COMMAND_RESULT");
|
||||
|
||||
Reference in New Issue
Block a user