export const LINUXCNC_TASK_POLICY = { apiName: "web-rtcp-5axis-linuxcnc-task-policy", sourceMode: "linuxcnc-task-source-referenced-policy", semanticBoundary: "linuxcnc_task_state_mode_command_gate", sourceReferences: [ { path: "linuxcnc/src/emc/nml_intf/emc.hh", symbols: ["EMC_TASK_MODE", "EMC_TASK_STATE", "EMC_TASK_INTERP"], }, { path: "linuxcnc/src/emc/task/emctaskmain.cc", symbols: [ "emcTaskPlan", "EMC_TASK_PLAN_RUN", "EMC_TASK_PLAN_EXECUTE", "EMC_TASK_PLAN_PAUSE", "EMC_TASK_PLAN_RESUME", "EMC_TASK_ABORT", "EMC_JOG_INCR", "EMC_JOINT_HOME", ], }, { path: "linuxcnc/src/emc/task/emctask.cc", symbols: ["emcTaskSetState", "determineState"], }, ], }; export const LINUXCNC_TASK_MODES = new Set(["manual", "auto", "mdi"]); export const LINUXCNC_TASK_STATES = new Set(["estop", "estop-reset", "off", "on"]); export const LINUXCNC_INTERP_STATES = new Set(["idle", "reading", "paused", "waiting"]); export function normalizeLinuxCncTaskMode(mode) { if (mode === "jog") return "manual"; return LINUXCNC_TASK_MODES.has(mode) ? mode : "manual"; } export function normalizeLinuxCncTaskState(machine = {}) { if (LINUXCNC_TASK_STATES.has(machine.taskState)) return machine.taskState; if (machine.estopActive) return "estop"; if (machine.powerOn) return "on"; return "estop-reset"; } export function normalizeLinuxCncInterpState(machine = {}, runState = "idle") { if (LINUXCNC_INTERP_STATES.has(machine.interpState)) return machine.interpState; if (runState === "running") return "reading"; if (runState === "paused" || runState === "stepping") return "paused"; return "idle"; } export function createLinuxCncTaskPolicyStatus(state) { const taskState = normalizeLinuxCncTaskState(state.machine); const taskMode = normalizeLinuxCncTaskMode(state.machine.mode); const interpState = normalizeLinuxCncInterpState(state.machine, state.runState); const allHomed = Boolean(state.machine.allHomed); const noForceHoming = Boolean(state.machine.noForceHoming); const machineFileStaging = state.machineFileStaging || {}; const profileId = state.profile?.id || state.machineProfile || null; const taskHalReadiness = state.taskHalRuntimeReadiness || {}; const taskHalRuntimeReady = Boolean( state.taskHalRuntime?.loaded && taskHalReadiness.taskRuntimeReady === true && taskHalReadiness.motionRuntimeReady === true && taskHalReadiness.halRuntimeReady === true, ); const iniLoaded = state.iniConfigReadiness?.loaded === true && state.iniConfigReadiness?.ready === true; const machineFileStaged = machineFileStaging.status === "staged" && Boolean(machineFileStaging.save?.files?.length); const machineFileOpened = profileId === "gmoccapy-xyzab" ? Boolean(machineFileStaging.selectedGcodeSourceRel) : Boolean(machineFileStaging.selectedGcodeSourceRel || state.activeProgram); const interpIdle = interpState === "idle"; const running = taskMode === "auto" && (state.runState === "running" || interpState === "reading" || interpState === "waiting"); const referenceOnly = state.profile?.tcpCapable === false || state.profile?.promotionAllowed === false; return { ...LINUXCNC_TASK_POLICY, taskState, taskMode, interpState, runState: state.runState || "idle", allHomed, noForceHoming, iniLoaded, machineFileStaged, machineFileOpened, taskHalRuntimeReady, interpIdle, referenceOnly, profileId, powerOn: taskState === "on", estopActive: taskState === "estop", canMove: taskState === "on", canJog: taskState === "on" && taskMode === "manual" && !running, canHome: taskState === "on" && taskMode === "manual" && !running, canRunAuto: taskState === "on" && taskMode === "auto" && (allHomed || noForceHoming), canRunAutoStrict: taskState === "on" && taskMode === "auto" && (allHomed || noForceHoming) && interpIdle && iniLoaded && machineFileStaged && machineFileOpened && taskHalRuntimeReady, canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming), canPause: taskState === "on" && (taskMode === "auto" || taskMode === "mdi"), canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && interpState === "paused", canAbort: true, canSpindle: taskState === "on", canCoolant: taskState === "on", canOverride: taskState === "on", canLeaveAuto: taskMode !== "auto" || interpState === "idle", }; } export function gateLinuxCncTaskAction(state, action) { const status = createLinuxCncTaskPolicyStatus(state); const type = typeof action === "string" ? action : action?.type; const requestedMode = typeof action === "object" ? action.mode : undefined; switch (type) { case "TOGGLE_POWER": if (status.taskState === "estop") { return block(status, "power on blocked: reset estop first"); } return allow(status); case "SET_MODE": return gateMode(status, requestedMode); case "JOG": if (status.taskState !== "on") return block(status, "jog blocked: machine must be on"); if (status.taskMode !== "manual") return block(status, "jog blocked: switch to manual mode first"); if (status.taskMode === "auto" && (status.runState === "running" || status.interpState === "reading" || status.interpState === "waiting")) { return block(status, "jog blocked: interpreter must be idle"); } return allow(status); case "HOME": if (status.taskState !== "on") return block(status, "home blocked: machine must be on"); if (status.taskMode !== "manual") return block(status, "home blocked: switch to manual mode first"); if (status.taskMode === "auto" && (status.runState === "running" || status.interpState === "reading" || status.interpState === "waiting")) { return block(status, "home blocked: interpreter must be idle"); } return allow(status); case "RUN_MDI": { const manualTouchOff = isManualTouchOffMdiAction(status, action); if (status.taskState !== "on") return block(status, "MDI blocked: machine must be on"); if (status.taskMode !== "mdi" && !manualTouchOff) return block(status, "MDI blocked: switch to MDI mode first"); if (!status.allHomed && !status.noForceHoming) return block(status, "MDI blocked: home machine first"); if (status.interpState === "reading" || status.interpState === "waiting") { return block(status, "MDI blocked: interpreter must be idle"); } return allow(status); } case "RUN": case "STEP": case "RUN_FRAME": if (status.taskState !== "on") return block(status, `${type.toLowerCase()} blocked: machine must be on`); if (status.taskMode !== "auto") return block(status, `${type.toLowerCase()} blocked: switch to auto mode first`); if (!status.allHomed && !status.noForceHoming) return block(status, `${type.toLowerCase()} blocked: home machine first`); if (type === "RUN" && status.interpState === "paused") return block(status, "run blocked: resume paused program first"); if (type === "RUN" && status.profileId === "gmoccapy-xyzab") { const strictGate = gateGmoccapyXyzabRun(status); if (!strictGate.allowed) return strictGate; } return allow(status); case "PAUSE": if (status.taskState !== "on") return block(status, "pause blocked: machine must be on"); if (status.taskMode !== "auto" && status.taskMode !== "mdi") { return block(status, "pause blocked: task mode must be auto or MDI"); } return allow(status); case "RESUME": if (status.taskState !== "on") return block(status, "resume blocked: machine must be on"); if (status.taskMode !== "auto" && status.taskMode !== "mdi") { return block(status, "resume blocked: task mode must be auto or MDI"); } if (status.interpState !== "paused") return block(status, "resume blocked: interpreter is not paused"); return allow(status); case "STOP": case "ABORT": return allow(status); case "SET_SPINDLE_DIRECTION": case "SPINDLE": return gateSpindle(status, typeof action === "object" ? action.direction : undefined); case "TOGGLE_COOLANT": case "COOLANT": if (status.taskState !== "on") return block(status, "coolant blocked: machine must be on"); return allow(status); case "ADJUST_OVERRIDE": case "ADJUST_SPINDLE_OVERRIDE": case "OVERRIDE": if (status.taskState !== "on") return block(status, "override blocked: machine must be on"); return allow(status); default: return allow(status); } } function isManualTouchOffMdiAction(status, action) { if (!action || action.manualTouchOff !== true || status.taskMode !== "manual") return false; const command = String(action.command || "") .replace(/\([^)]*\)/g, " ") .replace(/;.*$/g, " ") .trim() .replace(/\s+/g, " ") .toUpperCase(); return /^G10\s+L20\s+P0\s+[XYZABC]-?\d/.test(command) || /^G43(?:\s|$)/.test(command); } function gateGmoccapyXyzabRun(status) { if (!status.iniLoaded) return block(status, "run blocked: LinuxCNC INI not loaded"); if (!status.machineFileStaged) return block(status, "run blocked: LinuxCNC machine files not staged"); if (!status.machineFileOpened) return block(status, "run blocked: no machine-file G-code opened for task/HAL session"); if (!status.interpIdle) return block(status, "run blocked: interpreter must be idle"); if (!status.taskHalRuntimeReady) return block(status, "run blocked: task/HAL runtime not ready"); return allow(status); } function gateSpindle(status, direction = "stop") { if (status.taskState !== "on") return block(status, "spindle blocked: machine must be on"); const requested = String(direction || "stop").toLowerCase(); if ((requested === "forward" || requested === "reverse") && (status.interpState === "reading" || status.interpState === "waiting")) { return block(status, "spindle blocked: interpreter is running"); } return allow(status); } function gateMode(status, requestedMode) { const targetMode = normalizeLinuxCncTaskMode(requestedMode); if (!LINUXCNC_TASK_MODES.has(targetMode)) { return block(status, `mode blocked: invalid LinuxCNC task mode ${requestedMode}`); } if (status.taskMode === "auto" && status.interpState !== "idle" && targetMode !== "auto") { return block(status, "mode blocked: AUTO interpreter is not idle"); } if (status.taskState !== "on") { return block(status, `mode blocked: machine must be on before ${targetMode.toUpperCase()}`); } if ((targetMode === "mdi" || targetMode === "auto") && !status.allHomed && !status.noForceHoming) { return block(status, `mode blocked: home machine before ${targetMode.toUpperCase()}`); } return allow(status); } function allow(status) { return { allowed: true, status, operatorMessage: null, }; } function block(status, operatorMessage) { return { allowed: false, status, operatorMessage, }; }