|
|
|
|
@@ -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");
|
|
|
|
|
|