Files
linuxcncWebWASM/web/app/axis-contract.mjs

139 lines
12 KiB
JavaScript

// Source-aligned AXIS state and gate contract. This module is pure: it never
// sends a command and never treats a local click as authoritative state.
export const AXIS_TASK_STATES = Object.freeze(["ESTOP", "OFF", "ON"]);
export const AXIS_INTERP_STATES = Object.freeze(["IDLE", "READING", "WAITING", "PAUSED"]);
export const AXIS_MOTION_MODES = Object.freeze(["FREE", "COORD", "TELEOP"]);
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", 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",
reset: "RESET",
});
export const AXIS_BOUNDARIES = Object.freeze({
desktopWindow: "WEB-EQUIV", pythonCallbacks: "PY-WASM", virtualDevices: "SIM-DEVICE",
externalProcess: "HOST_BOUNDARY", physicalHardware: "REAL-EXCLUDED",
});
function string(value, fallback = "") { return typeof value === "string" ? value.toUpperCase() : fallback; }
function number(value, fallback = 0) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
export function normalizeAxisState(input = {}) {
const task = input.task ?? input.snapshot?.task ?? {};
const program = input.program ?? input.snapshot?.program ?? {};
const motion = input.motion ?? input.snapshot?.motion ?? {};
const devices = input.devices ?? input.snapshot?.devices ?? {};
const ioSource = input.io ?? input.snapshot?.io ?? input.device_io ?? input.deviceIo ?? input.snapshot?.device_io ?? {};
const ioState = ioSource?.state ?? ioSource ?? {};
const rawTaskState = string(task.state ?? input.task_state ?? input.state, "OFF");
const ioHasEstop = typeof ioState.estop === "boolean" || typeof ioState.estop === "number";
const ioEstop = ioHasEstop && Boolean(ioState.estop);
const ioHasReady = typeof ioState.ready === "boolean" || typeof ioState.ready === "number";
const ioReady = ioHasReady && Boolean(ioState.ready);
const ioEstopReset = ioHasEstop && !ioEstop && ioHasReady && !ioReady && Number(ioState.last_error) === -904;
const taskState = ioEstop ? "ESTOP" : ioEstopReset ? "ESTOP_RESET" : ioHasReady && !ioReady ? "OFF" : rawTaskState === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED", "ON"].includes(rawTaskState) ? "ON" : "OFF";
const inferredInterp = rawTaskState === "RUNNING" ? "READING" : rawTaskState === "PAUSED" ? "PAUSED" : "IDLE";
const interpState = string(task.interp_state ?? input.interp_state ?? input.interpreter_state, inferredInterp);
const mode = string(task.mode ?? input.task_mode ?? input.mode, "MANUAL");
const motionMode = string(motion.mode ?? input.motion_mode, "COORD");
const programStatus = string(program.program_status ?? input.program_status, "IDLE").toLowerCase();
const queue = number(task.queued_mdi_commands ?? input.queued_mdi_commands);
const queueLimit = Math.max(1, number(task.max_queued_mdi_commands ?? input.max_queued_mdi_commands, 10));
const homed = Array.isArray(motion.homed_axes)
? motion.homed_axes
: Array.isArray(motion.homed)
? motion.homed
: Array.isArray(input.homed)
? input.homed
: motion.homed === true
? [true, true, true]
: [];
const limits = input.on_any_limit ?? motion.on_any_limit ?? motion.limit ?? false;
const fault = rawTaskState === "FAULT" || Boolean(motion.fault || devices.fault || ioState.fault || input.fault);
const tool = devices.tool ?? input.tool ?? {};
const toolNumber = number(tool.current ?? input.tool_in_spindle);
const taskFile = Boolean(input.taskfile ?? program.path ?? program.source_hash ?? program.loaded ?? (program.program_status && String(program.program_status).toLowerCase() !== "idle"));
const hasEditor = input.has_editor !== false;
return Object.freeze({
taskState, interpState, mode, motionMode, programStatus, queue, queueLimit,
taskFile, hasEditor, onAnyLimit: Boolean(limits), toolLoaded: toolNumber > 0,
allHomed: homed.length > 0 && homed.every(Boolean), homed, fault,
});
}
export function axisGate(action, input = {}) {
const state = normalizeAxisState(input);
const estop = state.taskState === "ESTOP";
const machineOn = state.taskState === "ON";
const idle = state.interpState === "IDLE";
const running = !idle;
const manualOk = machineOn && !running && (idle || (state.mode === "MDI" && state.queue < state.queueLimit));
const trajectoryTouchOffAllowed = state.motionMode !== "FREE" || input.kinematicsIdentity === true;
let enabled = false;
let reason = "axis_gate_unknown_action";
if (state.fault && !["estop", "recover", "overrideLimits"].includes(action)) return Object.freeze({ action, enabled: false, reason: "axis_fault_active", taskState: state.taskState, interpState: state.interpState });
switch (action) {
case "estop": enabled = true; reason = estop ? "axis_estop_reset_available" : "axis_estop_available"; break;
case "recover": enabled = true; reason = "axis_recover_available"; break;
case "power": {
const requested = input.payload?.active;
enabled = requested === false ? machineOn && !running : requested === true ? !estop && !machineOn && !running : !estop && !running;
reason = estop ? "axis_estop_active" : running ? "axis_interp_running" : enabled ? "axis_power_available" : requested === false ? "axis_machine_off" : "axis_machine_on";
break;
}
case "override": enabled = machineOn && !estop; reason = enabled ? "axis_override_available" : estop ? "axis_estop_active" : "axis_machine_off"; break;
case "reset": enabled = machineOn && !running && !state.onAnyLimit; reason = enabled ? "axis_reset_available" : state.onAnyLimit ? "axis_limit_active" : "axis_reset_state_gate"; break;
case "open": enabled = idle; reason = enabled ? "axis_interp_idle" : "axis_interp_running"; break;
case "load": enabled = machineOn && idle; reason = enabled ? "axis_program_load_available" : !machineOn ? "axis_machine_off" : "axis_interp_running"; break;
case "reload": enabled = idle && state.taskFile; reason = !idle ? "axis_interp_running" : state.taskFile ? "axis_reload_available" : "axis_taskfile_missing"; break;
case "edit": enabled = idle && state.taskFile && state.hasEditor; reason = !idle ? "axis_interp_running" : !state.taskFile ? "axis_taskfile_missing" : !state.hasEditor ? "axis_editor_missing" : "axis_edit_available"; break;
case "run": enabled = machineOn && idle && state.taskFile; reason = !machineOn ? "axis_machine_off" : !idle ? "axis_interp_running" : !state.taskFile ? "axis_taskfile_missing" : "axis_run_available"; break;
case "runLine": enabled = machineOn && idle && state.taskFile && input.highlight_line !== undefined && input.highlight_line !== null; reason = !state.taskFile ? "axis_taskfile_missing" : input.highlight_line === undefined || input.highlight_line === null ? "axis_highlight_missing" : enabled ? "axis_run_line_available" : "axis_machine_or_interp_gate"; break;
case "step": enabled = machineOn && state.taskFile; reason = !machineOn ? "axis_machine_off" : !state.taskFile ? "axis_taskfile_missing" : "axis_step_available"; break;
case "pause": enabled = machineOn && (state.interpState === "READING" || state.interpState === "WAITING"); reason = enabled ? "axis_pause_available" : "axis_pause_state_gate"; break;
case "resume": enabled = machineOn && state.interpState === "PAUSED"; reason = enabled ? "axis_resume_available" : "axis_resume_state_gate"; break;
case "stop": case "abort": enabled = machineOn && running; reason = enabled ? "axis_stop_available" : "axis_stop_state_gate"; break;
case "restart": enabled = machineOn && idle && state.taskFile; reason = enabled ? "axis_restart_available" : !machineOn ? "axis_machine_off" : !state.taskFile ? "axis_taskfile_missing" : "axis_restart_state_gate"; break;
case "mdi": enabled = machineOn && state.queue < state.queueLimit && (idle || state.mode === "MDI"); reason = enabled ? "axis_mdi_available" : estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : state.queue >= state.queueLimit ? "axis_mdi_queue_full" : "axis_interp_running"; break;
case "jogStart": case "jogStop": case "home": case "spindle": case "coolant": enabled = manualOk && state.mode === "MANUAL"; reason = enabled ? "axis_manual_available" : estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : "axis_manual_state_gate"; break;
case "touchOff": enabled = manualOk && state.mode === "MANUAL" && trajectoryTouchOffAllowed; reason = !manualOk ? "axis_manual_state_gate" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : !trajectoryTouchOffAllowed ? "axis_joint_mode_required" : "axis_touch_off_available"; break;
case "toolTouchOff": enabled = manualOk && state.mode === "MANUAL" && trajectoryTouchOffAllowed && state.toolLoaded; reason = !state.toolLoaded ? "axis_tool_missing" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : !trajectoryTouchOffAllowed ? "axis_joint_mode_required" : !manualOk ? "axis_manual_state_gate" : "axis_tool_touch_off_available"; break;
case "overrideLimits": enabled = state.onAnyLimit; reason = enabled ? "axis_limit_override_available" : "axis_no_active_limit"; break;
default: break;
}
return Object.freeze({ action, enabled, reason, taskState: state.taskState, interpState: state.interpState });
}
export function axisGateMatrix(input = {}) {
return Object.freeze(Object.fromEntries(Object.keys(AXIS_COMMANDS).map((action) => [action, axisGate(action, input)])));
}
export const XYZAC_GATE_ACTIONS = Object.freeze(["switchkinsIdentity", "switchkinsXyzac", "switchkinsUserk", "vismachClear"]);
export const XYZAC_COMMANDS = Object.freeze({ switchkinsIdentity: "KINEMATICS_SELECT", switchkinsXyzac: "KINEMATICS_SELECT", switchkinsUserk: "KINEMATICS_SELECT", vismachClear: "VISUAL_CLEAR" });
export function xyzacGate(action, input = {}) {
const state = normalizeAxisState(input);
const xyzac = input.xyzac ?? {};
const idle = state.interpState === "IDLE" && state.taskState === "ON";
const offsets = xyzac.finite_offsets !== false;
const pinVars = xyzac.hal_pin_vars === true;
const net = xyzac.switchkins_net === true;
const coordinates = xyzac.coordinates === "XYZAC" && xyzac.joint_count === 5;
if (action === "vismachClear") return Object.freeze({ action, enabled: true, reason: "axis_visual_only", visual_only: true, taskState: state.taskState, interpState: state.interpState });
const common = idle && pinVars && net && offsets;
const commonReason = state.taskState === "ESTOP" ? "xyzac_estop_active" : state.taskState !== "ON" ? "xyzac_machine_off" : !idle ? "xyzac_interp_not_idle" : !pinVars ? "xyzac_hal_pin_vars_missing" : !net ? "xyzac_switchkins_net_missing" : !offsets ? "xyzac_offsets_nonfinite" : "xyzac_switchkins_common_gate";
if (action === "switchkinsIdentity") return Object.freeze({ action, enabled: common, reason: common ? "xyzac_m429_available" : commonReason, required_type: 0, taskState: state.taskState, interpState: state.interpState });
if (action === "switchkinsXyzac") return Object.freeze({ action, enabled: common && coordinates, reason: !common ? commonReason : !coordinates ? "xyzac_coordinates_invalid" : "xyzac_m428_available", required_type: 1, taskState: state.taskState, interpState: state.interpState });
if (action === "switchkinsUserk") return Object.freeze({ action, enabled: common && Boolean(xyzac.userk_capability), reason: !common ? commonReason : !xyzac.userk_capability ? "xyzac_userk_unavailable" : "xyzac_m430_available", required_type: 2, taskState: state.taskState, interpState: state.interpState });
return Object.freeze({ action, enabled: false, reason: "xyzac_gate_unknown_action", taskState: state.taskState, interpState: state.interpState });
}
export function xyzacGateMatrix(input = {}) {
return Object.freeze(Object.fromEntries(XYZAC_GATE_ACTIONS.map((action) => [action, xyzacGate(action, input)])));
}