259 lines
11 KiB
JavaScript
259 lines
11 KiB
JavaScript
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,
|
|
};
|
|
}
|