// AXIS-facing state names are a view over the native task and interpreter // states. Keep sim_task.v1 unchanged and expose this as an additive payload. export const AXIS_STATES = Object.freeze(["ESTOP", "ESTOP_RESET", "OFF", "ON", "HOME", "IDLE", "RUNNING", "PAUSED"]); export const LINUXCNC_STATE_CONSTANTS = Object.freeze({ STATE_ESTOP: 1, STATE_ESTOP_RESET: 2, STATE_OFF: 3, STATE_ON: 4, MODE_MANUAL: 1, MODE_AUTO: 2, MODE_MDI: 3, INTERP_IDLE: 1, INTERP_READING: 2, INTERP_PAUSED: 3, INTERP_WAITING: 4, }); export const AXIS_TRANSITIONS = Object.freeze({ ESTOP: ["ESTOP", "ESTOP_RESET"], ESTOP_RESET: ["ESTOP", "ON", "OFF"], OFF: ["OFF", "ESTOP", "ESTOP_RESET"], ON: ["ON", "OFF", "ESTOP", "HOME", "IDLE", "RUNNING", "PAUSED"], HOME: ["HOME", "ON", "IDLE", "ESTOP"], IDLE: ["IDLE", "RUNNING", "PAUSED", "HOME", "ESTOP", "OFF"], RUNNING: ["RUNNING", "PAUSED", "IDLE", "ESTOP"], PAUSED: ["PAUSED", "RUNNING", "IDLE", "ESTOP"], }); function programActivity(task, program) { if (task?.state === "PAUSED" || program?.program_status === "paused") return "PAUSED"; if (task?.state === "RUNNING" || program?.program_status === "running") return "RUNNING"; return "IDLE"; } export function axisState({ task = {}, program = {}, motion = {}, devices = {}, device_io = {}, io = {}, action = null, payload = {} } = {}) { const taskState = String(task.state ?? "NEW").toUpperCase(); const ioState = io?.state ?? io ?? device_io?.state ?? device_io ?? {}; const hasIoState = typeof ioState.estop === "boolean" || typeof ioState.estop === "number"; const ioEstop = hasIoState && Boolean(ioState.estop); const ioReady = typeof ioState.ready === "boolean" || typeof ioState.ready === "number" ? Boolean(ioState.ready) : null; // A powered-off machine is OFF. ESTOP_RESET is distinct and is only // reported after the WASM IO latch has recorded the ESTOP edge. const estopReset = hasIoState && !ioEstop && ioReady === false && Number(ioState.last_error) === -904; let machineState = ioEstop || ["ESTOP", "FAULT"].includes(taskState) ? "ESTOP" : estopReset ? "ESTOP_RESET" : ioReady === false ? "OFF" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF"; let operationState = programActivity(task, program); const state = machineState === "ESTOP" || machineState === "ESTOP_RESET" ? machineState : operationState === "HOME" ? "HOME" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "OFF" ? "OFF" : "IDLE"; const sourceTaskState = machineState === "ESTOP" ? "STATE_ESTOP" : machineState === "ESTOP_RESET" ? "STATE_ESTOP_RESET" : machineState === "OFF" ? "STATE_OFF" : "STATE_ON"; const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? "INTERP_READING" : "INTERP_IDLE"; const sourceMode = String(task.mode ?? "MANUAL").toUpperCase(); return Object.freeze({ state, machine_state: machineState, operation_state: operationState, source_task_state: sourceTaskState, source_interp_state: sourceInterpState, source_mode: sourceMode, states: [...AXIS_STATES] }); } export function validateAxisTransition(previous, next) { if (!AXIS_STATES.includes(previous) || !AXIS_STATES.includes(next)) return { status: "blocked", code: "axis_state_unknown" }; return AXIS_TRANSITIONS[previous]?.includes(next) ? { status: "accepted", previous, next } : { status: "blocked", code: "axis_state_transition_invalid", previous, next }; }