Enforce LinuxCNC WASM XYZAC command authority
This commit is contained in:
@@ -149,7 +149,7 @@ export class SimSession {
|
||||
motion_authority: statePayload.motion_authority ?? this.motionAuthority,
|
||||
task_authority: statePayload.task_authority ?? this.task.authority(),
|
||||
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}),
|
||||
axis_state: axisState({ task, program, action: input.kind, payload: input.payload ?? {} }),
|
||||
axis_state: axisState({ task, program, motion: this.motionProjection(), devices: this.wasmDeviceProjection(), io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -220,17 +220,41 @@ export class SimSession {
|
||||
const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
const homeLimit = this.wasmHomeLimit.read();
|
||||
const ioState = this.xyzacIo?.canonical?.() ?? {};
|
||||
const devices = this.wasmDeviceProjection();
|
||||
const deviceIo = this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export();
|
||||
const halRuntime = this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export();
|
||||
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 ?? {}, action: input.kind, payload: input.payload ?? {} });
|
||||
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, action: input.kind, payload: input.payload ?? {} });
|
||||
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, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
|
||||
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"];
|
||||
const gates = { ...Object.fromEntries(gateActions.map((action) => [action, axisGate(action, gateInput)])), ...xyzacGateMatrix(gateInput) };
|
||||
const gates = this.xyzacProgram
|
||||
? {
|
||||
load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }),
|
||||
run: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }),
|
||||
runLine: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }),
|
||||
step: this.xyzacCommandGate({ kind: "PROGRAM_STEP", payload: {} }),
|
||||
pause: this.xyzacCommandGate({ kind: "PROGRAM_PAUSE", payload: {} }),
|
||||
resume: this.xyzacCommandGate({ kind: "PROGRAM_RESUME", payload: {} }),
|
||||
abort: this.xyzacCommandGate({ kind: "PROGRAM_ABORT", payload: {} }),
|
||||
restart: this.xyzacCommandGate({ kind: "PROGRAM_RESTART", payload: {} }),
|
||||
mdi: this.xyzacCommandGate({ kind: "MDI_EXECUTE", payload: {} }),
|
||||
jogStart: this.xyzacCommandGate({ kind: "JOG_START", payload: {} }),
|
||||
jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }),
|
||||
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) } }),
|
||||
powerOn: this.xyzacCommandGate({ kind: "POWER", payload: { active: true } }),
|
||||
powerOff: this.xyzacCommandGate({ kind: "POWER", payload: { active: false } }),
|
||||
reset: this.xyzacCommandGate({ kind: "RESET", payload: {} }),
|
||||
...Object.fromEntries([0, 1, 2].map((type) => [type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : "switchkinsUserk", this.xyzacCommandGate({ kind: "KINEMATICS_SELECT", payload: { type } })])),
|
||||
vismachClear: { action: "vismachClear", enabled: true, reason: "axis_visual_only", visual_only: true },
|
||||
}
|
||||
: { ...Object.fromEntries(gateActions.map((action) => [action, axisGate(action, gateInput)])), ...xyzacGateMatrix(gateInput) };
|
||||
const wasmPosition = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
|
||||
? wasmMotion.state.position
|
||||
: authoritative.kinematics.pose;
|
||||
@@ -308,7 +332,12 @@ export class SimSession {
|
||||
const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {};
|
||||
const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
|
||||
const devices = this.wasmDeviceProjection();
|
||||
// The selected XYZAC case is governed by the native WASM task/IO/program
|
||||
// handlers. JS only receives the result; it must not pre-reject a command
|
||||
// using a duplicate gate implementation.
|
||||
if (this.xyzacProgram && input.kind !== "VISUAL_CLEAR") return this.xyzacCommandGate(input);
|
||||
if (input.kind === "KINEMATICS_SELECT" || input.kind === "VISUAL_CLEAR") {
|
||||
if (input.kind === "KINEMATICS_SELECT" && ![0, 1, 2].includes(Number(input.payload?.type))) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" };
|
||||
const type = Number(input.payload?.type);
|
||||
const action = input.kind === "VISUAL_CLEAR" ? "vismachClear" : type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : type === 2 ? "switchkinsUserk" : null;
|
||||
if (!action) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" };
|
||||
@@ -326,6 +355,71 @@ export class SimSession {
|
||||
});
|
||||
}
|
||||
|
||||
xyzacCommandGate(input) {
|
||||
const task = this.task.export();
|
||||
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 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 } };
|
||||
}
|
||||
|
||||
stateForCheckpoint() {
|
||||
const state = {
|
||||
context_id: this.contextId,
|
||||
@@ -887,6 +981,7 @@ export class SimSession {
|
||||
}
|
||||
try {
|
||||
if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904);
|
||||
else if (kind === "ESTOP" && payload?.active === false && result.status === "verified" && this.xyzacProgram && this.task.state.state === "ESTOP") this.task.transition("STOPPED");
|
||||
else if (kind === "IO_RECOVER" && result.status === "verified") this.task.recover();
|
||||
else if (kind === "TOOL_FAULT" && payload?.fault === true) this.task.fault(-905);
|
||||
} catch (error) {
|
||||
@@ -945,14 +1040,31 @@ export class SimSession {
|
||||
try { this.task.transition("READY"); } catch (error) { return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed"); }
|
||||
return finish(this.envelope("SESSION_READY", input, { state_hash: this.task.hash(), task: this.task.export(), subscriptions: [] }), "verified");
|
||||
case "SET_MODE": {
|
||||
if (this.xyzacProgram) {
|
||||
const gateResult = this.xyzacCommandGate(input);
|
||||
if (!gateResult.enabled) return finish(this.envelope("ERROR", input, { status: "blocked", code: "axis_gate", gate_reason: gateResult.reason, message_key: gateResult.reason, operation_id: input.operation_id ?? null }), "failed");
|
||||
}
|
||||
const mode = String(input.payload?.mode ?? "").toUpperCase();
|
||||
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
|
||||
try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
|
||||
}
|
||||
case "POWER": {
|
||||
if (this.xyzacProgram) {
|
||||
const result = this.xyzacIo.command("POWER", input.payload ?? {});
|
||||
if (result.status === "verified") {
|
||||
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");
|
||||
}
|
||||
} catch { /* the WASM IO state remains authoritative */ }
|
||||
if (this.xyzacDeviceIo) this.xyzacDeviceIo.command("INTERLOCK_RESET", input.payload ?? {}, input.operation_id);
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, active: input.payload?.active === true, task: this.task.export(), device_io: this.xyzacIo.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
if (input.payload?.active !== true) return finish(this.error(input, "power_state", "power_active_required"), "failed");
|
||||
const result = this.xyzacIo ? this.xyzacIo.command("IO_RECOVER", input.payload ?? {}) : this.devices.command("IO_RECOVER", input.payload ?? {});
|
||||
const result = this.xyzacIo ? this.xyzacIo.command("POWER", input.payload ?? {}) : this.devices.command("IO_RECOVER", input.payload ?? {});
|
||||
if (result.status === "verified") {
|
||||
try { this.task.recover(); } catch { /* READY is already powered for the selected simulation. */ }
|
||||
if (this.xyzacDeviceIo) this.xyzacDeviceIo.command("INTERLOCK_RESET", input.payload ?? {}, input.operation_id);
|
||||
@@ -960,6 +1072,13 @@ export class SimSession {
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, active: true, task: this.task.export(), device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "RESET": {
|
||||
if (this.xyzacProgram) {
|
||||
const result = this.xyzacIo.command("RESET");
|
||||
if (result.status === "verified" && ["FAULT", "STOPPED"].includes(this.task.state.state)) {
|
||||
try { this.task.recover(); } catch { /* retain the WASM IO result */ }
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, task: this.task.export(), device_io: this.xyzacIo.export(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "reset_while_program_active"), "failed");
|
||||
const result = this.motion.resetFault();
|
||||
if (result.status !== "verified") return finish(this.envelope("ERROR", input, { ...result, command: input.kind, task: this.task.export() }), "failed");
|
||||
@@ -1023,6 +1142,8 @@ export class SimSession {
|
||||
this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, limits: configuration.home_limits });
|
||||
this.xyzacIo?.destroy();
|
||||
this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: 2, toolCapacity: 1001 });
|
||||
const initialPower = this.xyzacIo.command("POWER", { active: false });
|
||||
if (initialPower.status !== "verified") throw new Error(initialPower.code ?? "wasm_power_off_init");
|
||||
this.xyzacDeviceIo?.destroy();
|
||||
this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 });
|
||||
this.xyzacProgram?.destroy();
|
||||
|
||||
Reference in New Issue
Block a user