Files
cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src/state/store.js
2026-07-07 18:45:26 -04:00

6431 lines
234 KiB
JavaScript

import { buildRtcpFrame } from "../runtime/rtcp-frame.js";
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../runtime/linuxcnc-boundary-adapter.js";
import { createFullLinuxCncExecutionBoundary } from "../runtime/full-execution-boundary.js";
import {
DEFAULT_SESSION_FILENAME,
DEFAULT_SESSION_ID,
createFiveAxisSessionPayload,
loadFiveAxisSessionSnapshot,
restoreFiveAxisSessionState,
saveFiveAxisSessionSnapshot,
} from "../runtime/five-axis-session.js";
import { fiveAxisProfiles, getFiveAxisProfile } from "../profiles/index.js";
import { applyIniConfigToProfile } from "../runtime/linuxcnc-ini-runtime.js";
import {
listLinuxCncGcodeSources,
listProjectGcodeFiles,
selectMachineFileProgram,
stageProfileMachineFiles,
} from "../runtime/linuxcnc-machine-file-staging.js";
import {
buildTaskHalProgramMotionPlan,
buildTaskHalSessionFromMachineFiles,
} from "../runtime/linuxcnc-task-hal-runtime.js";
import {
applyToolCommandSequence,
createToolDbReadiness,
createToolDbSimulation,
createToolRuntimeState,
editToolEntry,
extractToolCommandSequenceFromProgram,
listToolEntries,
parseLinuxCncToolTable,
queryToolEntry,
saveToolDbSimulation,
} from "../runtime/tool-db-simulation.js";
import {
createControlledUserMReadiness,
createControlledUserMSimulation,
runControlledUserM,
runControlledUserMProgramScan,
} from "../runtime/controlled-user-m-simulation.js";
import { createLinuxCncParityMatrix } from "../runtime/linuxcnc-parity-matrix.js";
import { gmoccapyHalModel, resolveGmoccapyHardwareButton } from "../runtime/gmoccapy-hal-model.js";
import {
createLinuxCncTaskPolicyStatus,
deriveLinuxCncTaskState,
gateLinuxCncTaskAction,
normalizeLinuxCncTaskMode,
} from "./linuxcnc-task-policy.js";
import { buildProgramExecutionTiming, timingAtMotionIndex } from "../runtime/execution-timing.js";
import {
buildAxisExecutionTraceFromProgram,
buildAxisPreviewPathFromProgram,
} from "../runtime/axis-preview-path.js";
const defaultProfile = getFiveAxisProfile("xyzbc-trt");
const initialLinuxCncBoundaryAdapter = createLinuxCncBoundaryAdapter({
profile: defaultProfile,
});
const initialLinuxCncBoundaryReadiness = createLinuxCncBoundaryReadiness(initialLinuxCncBoundaryAdapter);
const initialControlledUserMSimulation = createControlledUserMSimulation();
const MACHINE_PROJECT_OPFS_ROOT = "web-rtcp-5axis-xyzbc-trt-sim-plan/machines";
const initialAxisPose = {
x: 0.0,
y: 0.0,
z: 0.0,
a: 0.0,
b: 0.0,
c: 0.0,
};
function createTaskHalStatusLoopState({
active = false,
sequence = 0,
profileId = null,
iniPath = null,
kinematicsModuleId = null,
tickCount = 0,
batchSize = 5,
intervalMs = 25,
taskPeriodNs = 10000000,
servoPeriodNs = 1000000,
lastStatusAt = null,
lastError = null,
stopReason = null,
} = {}) {
return {
apiName: "web-rtcp-5axis-task-hal-status-loop",
active,
sequence,
profileId,
iniPath,
kinematicsModuleId,
tickCount,
batchSize,
intervalMs,
taskPeriodNs,
servoPeriodNs,
lastStatusAt,
lastError,
stopReason,
semanticBoundary: "js_status_polling_loop_for_linuxcnc_task_hal_motion_status",
};
}
function withDefaultWebOpfsRequirement(options = {}) {
if (options.storage || options.requireOpfs !== undefined) {
return options;
}
const isBrowser = typeof globalThis.window === "object" || typeof globalThis.document === "object";
return isBrowser ? { ...options, requireOpfs: true } : options;
}
function buildProgramAxisPathFromProgram(options = {}) {
return buildAxisExecutionTraceFromProgram(options)
|| buildAxisPreviewPathFromProgram(options);
}
const initialState = {
machineProfile: "xyzbc-trt",
availableProfiles: fiveAxisProfiles.map(({ id, title, traj, kinematicsModuleId, kinematics }) => ({
id,
title,
coordinates: traj.coordinates,
kinematicsModuleId: kinematicsModuleId || id,
kinematics,
})),
profile: defaultProfile,
sessionName: "xyzbc-trt-web-session",
sessionPersistence: {
apiName: "web-rtcp-5axis-session-persistence-state",
sessionId: DEFAULT_SESSION_ID,
filename: DEFAULT_SESSION_FILENAME,
status: "not-saved",
path: null,
storageMode: null,
storageCapability: null,
savedAt: null,
restoredAt: null,
lastError: null,
},
machineFileStaging: {
apiName: "web-rtcp-5axis-machine-file-staging-state",
status: "not-staged",
profileId: null,
fileCount: 0,
opfsRoot: null,
storageMode: null,
storageCapability: null,
savedAt: null,
lastError: null,
plan: null,
save: null,
gcodeSources: [],
gcodeFiles: [],
selectedGcodeSourceRel: null,
},
machineProject: null,
programValidation: null,
linuxCncParityMatrix: null,
rightSidebarEntrances: [],
sourceMode: "fixture-ui-only",
frameSourceMode: "fixture-ui-only",
desiredFrameSourceMode: "fixture-ui-only",
machine: {
powerOn: false,
estopActive: false,
motionEnabled: false,
taskState: "estop-reset",
mode: "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
resumeInhibit: false,
manualPanel: "manual",
allHomed: false,
homed: [false, false, false, false, false],
noForceHoming: false,
homing: false,
homeState: "unhomed",
selectedJoint: 0,
jogAxis: "x",
jogIncrement: 1,
mdiCommand: "G0 X0 Y0 Z0",
mdiDistanceMode: "absolute",
resetCount: 0,
},
runState: "idle",
activeProgram: "./demos/xyzbc_switchkins.ngc",
programSource: "linuxcnc-axis-default",
programStartLine: 1,
activeLine: 2,
lineCount: 3,
fileSizeBytes: 109,
kinsType: "identity",
rtcpState: "off",
axisPose: initialAxisPose,
jointPose: [],
tcpPose: {
x: 0.0,
y: 0.0,
z: 0.0,
a: 0.0,
c: 0.0,
},
toolAxisVector: {
x: 0.0,
y: 0.0,
z: 1.0,
},
rtcpFrame: null,
kinematicsRuntime: null,
kinematicsRuntimeReadiness: null,
kinematicsExecutionContext: "none",
interpreterRuntime: null,
interpreterRuntimeReadiness: null,
programExecution: null,
programExecutionTiming: null,
programAxisPreviewPath: null,
programElapsedSeconds: 0,
programRemainingSeconds: 0,
programExecutionSourceMode: "fixture-line-playback",
programExecutionMotionIndex: 0,
programExecutionSampleIndex: 0,
programRuntimeFeedback: null,
programRuntimeFeedbackHistory: [],
programLineExecution: {},
programUiExecution: null,
linuxCncProcessMonitor: null,
taskHalRuntime: null,
taskHalRuntimeReadiness: null,
taskHalStatus: null,
taskHalSession: null,
taskHalExecutionPending: false,
taskHalExecutionSequence: 0,
taskHalStatusLoop: createTaskHalStatusLoopState(),
taskHalPauseLock: null,
taskHalFallbackReason: null,
pendingJogCommand: null,
interpreterExecutionPending: false,
interpreterExecutionSequence: 0,
machineFileExecution: null,
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: 84.019,
}),
controlledUserMSimulation: initialControlledUserMSimulation,
controlledUserMReadiness: createControlledUserMReadiness(initialControlledUserMSimulation),
fullExecutionBoundary: null,
linuxCncTaskPolicy: null,
asyncFrameRefreshPending: false,
asyncFrameRefreshSequence: 0,
lastKinematicsResult: null,
linuxCncBoundaryAdapter: initialLinuxCncBoundaryAdapter,
linuxCncBoundaryReadiness: initialLinuxCncBoundaryReadiness,
linuxCncIniConfig: null,
iniConfigReadiness: {
apiName: "web-rtcp-5axis-ini-config-readiness",
loaded: false,
ready: false,
path: null,
missing: ["LinuxCNC INI not loaded"],
},
dro: {
x: 0.0,
y: 0.0,
z: 0.0,
a: 0.0,
b: 0.0,
c: 0.0,
tcpX: 43.0,
tcpY: -32.15,
tcpZ: -11.306,
dtgX: 0.0,
dtgY: 0.01,
dtgZ: 2.25,
},
feed: {
currentVelocity: 0,
rapidOverride: 100,
feedRate: 4500000,
feedOverride: 100,
},
spindle: {
rpm: 1600,
override: 100,
enabled: false,
direction: "stop",
halPins: {
on: 0,
forward: 0,
reverse: 0,
speedOut: 0,
atSpeed: 0,
},
},
coolant: {
flood: false,
mist: false,
},
gmoccapyGui: {
ignoreLimits: false,
optionalBlocks: false,
optionalStop: false,
feedOverrideCounts: 0,
rapidOverrideCounts: 0,
spindleOverrideCounts: 0,
jogVelocityCounts: 0,
feedOverrideCountEnabled: false,
rapidOverrideCountEnabled: false,
spindleOverrideCountEnabled: false,
jogVelocityCountEnabled: false,
feedOverrideAnalogEnabled: false,
rapidOverrideAnalogEnabled: false,
spindleOverrideAnalogEnabled: false,
jogVelocityAnalogEnabled: false,
jogVelocity: 100,
jogIncrementIndex: 0,
jogIncrementLabel: "Continuous",
jogIncrementOutput: 0,
turtleJog: false,
activeJogPin: null,
settingsUnlockMode: "use",
settingsUnlockPin: false,
setupSensitive: true,
toolsensorConfigured: false,
toolMeasurement: false,
probeHeight: 0,
blockHeight: 0,
searchVelocity: 0,
probeVelocity: 0,
userMessagesConfigured: false,
userMessagePins: [],
warningConfirm: false,
error: false,
deletedMessageCount: 0,
activeNativePage: "main",
nativePageMode: "implemented",
filePageStatus: "main",
filePageLastAction: null,
macroPageConfigured: true,
macroButtonsEnabled: true,
macroLastCommand: null,
macroLastName: null,
toolEditorStatus: "closed",
toolEditorWritebackEnabled: false,
toolEditorLastAction: null,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: null,
},
preview: {
pathPoints: 64,
selectedView: "iso",
fullscreen: false,
cameraRevision: 0,
},
toolPreview: {
toolNumber: 1,
diameter: 6,
length: 84.019,
units: "mm",
holder: "CAT40",
},
operatorMessage: "ready",
mdiHistory: [],
};
initialState.linuxCncTaskPolicy = createLinuxCncTaskPolicyStatus(initialState);
initialState.machineProject = createMachineProjectState(initialState);
initialState.programValidation = createProgramValidationState(initialState);
initialState.rightSidebarEntrances = createRightSidebarEntranceState(initialState, initialState.linuxCncTaskPolicy);
initialState.linuxCncParityMatrix = createLinuxCncParityMatrix(initialState);
initialState.linuxCncProcessMonitor = createLinuxCncProcessMonitor(initialState);
const programLines = [
"; zmax zmin r frate n a b c dist",
"o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
"m2",
];
export function createSimulationStore(seed = {}) {
const seedAxisPose = seed.axisPose || initialAxisPose;
const seedKinsType = seed.kinsType || initialState.kinsType;
const seedRtcpState = seed.rtcpState || initialState.rtcpState;
const seedFrame = buildRtcpFrame({
axisPose: seedAxisPose,
activeLine: seed.activeLine || initialState.activeLine,
kinsType: seedKinsType,
rtcpEnabled: seedRtcpState === "on" || seedKinsType === "tcp-xyzac",
sourceMode: seed.desiredFrameSourceMode || seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
});
let state = {
...initialState,
...seed,
axisPose: seedFrame.axisPose,
jointPose: seedFrame.jointPose,
tcpPose: seedFrame.tcpPose,
toolAxisVector: seedFrame.toolAxisVector,
rtcpState: seedFrame.rtcpState,
rtcpFrame: seedFrame,
fullExecutionBoundary: null,
dro: buildDroFromFrame(seedFrame, seed.programRuntimeFeedback || initialState.programRuntimeFeedback),
programLines: seed.programLines || programLines,
};
const initialAxisPreviewPath = seed.programAxisPreviewPath === undefined
? buildProgramAxisPathFromProgram({
filename: state.activeProgram,
sourceRel: state.programSourceRel,
content: state.programLines.join("\n"),
tool: currentPathTool(state),
})
: seed.programAxisPreviewPath;
state = {
...state,
machine: normalizeMachineForLinuxCncTask(state.machine, state.runState),
programAxisPreviewPath: initialAxisPreviewPath,
preview: {
...state.preview,
pathPoints: initialAxisPreviewPath?.sampleCount || state.preview.pathPoints,
},
};
state.linuxCncTaskPolicy = createLinuxCncTaskPolicyStatus(state);
state.machineProject = createMachineProjectState(state);
state.programValidation = createProgramValidationState(state);
state.rightSidebarEntrances = createRightSidebarEntranceState(state, state.linuxCncTaskPolicy);
state.linuxCncParityMatrix = createLinuxCncParityMatrix(state);
state.linuxCncProcessMonitor = createLinuxCncProcessMonitor(state);
state.fullExecutionBoundary = createFullLinuxCncExecutionBoundary(state);
const listeners = new Set();
let taskHalStatusLoopTimer = null;
const notify = () => {
for (const listener of listeners) {
listener(state);
}
};
const setState = (patch) => {
const merged = { ...state, ...patch };
const mergedMachine = normalizeMachineForLinuxCncTask(merged.machine, merged.runState);
const next = {
...merged,
machine: mergedMachine,
axisPose: clampAxisPoseToProfile(merged.axisPose, merged.profile),
};
const frameState = buildFrameForState(next, patch);
const frame = patch.rtcpFrame || frameState.frame;
const baseState = {
...next,
sourceMode: frame.sourceMode,
frameSourceMode: frame.sourceMode,
desiredFrameSourceMode: next.desiredFrameSourceMode || frame.sourceMode,
axisPose: frame.axisPose,
jointPose: frame.jointPose,
tcpPose: frame.tcpPose,
toolAxisVector: frame.toolAxisVector,
rtcpState: frame.rtcpState,
rtcpFrame: frame,
lastKinematicsResult: frameState.lastKinematicsResult,
dro: buildDroFromFrame(frame, next.programRuntimeFeedback),
};
const taskPolicy = createLinuxCncTaskPolicyStatus(baseState);
const projectState = createMachineProjectState(baseState);
const validationState = createProgramValidationState(baseState);
const sidebarState = createRightSidebarEntranceState(baseState, taskPolicy);
const nextState = {
...baseState,
linuxCncTaskPolicy: taskPolicy,
machineProject: projectState,
programValidation: validationState,
rightSidebarEntrances: sidebarState,
};
state = {
...nextState,
linuxCncParityMatrix: createLinuxCncParityMatrix(nextState),
linuxCncProcessMonitor: createLinuxCncProcessMonitor(nextState),
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
};
notify();
scheduleAsyncKinematicsRefresh();
};
const waitForStatePredicate = (predicate, timeoutMs = 10000) => {
if (predicate(state)) return Promise.resolve(state);
return new Promise((resolve, reject) => {
const startedAt = Date.now();
const listener = (nextState) => {
if (predicate(nextState)) {
listeners.delete(listener);
resolve(nextState);
return;
}
if (Date.now() - startedAt > timeoutMs) {
listeners.delete(listener);
reject(new Error("timed out waiting for store state"));
}
};
listeners.add(listener);
});
};
const waitForStatePolling = (predicate, timeoutMs = 10000, intervalMs = 25) => {
if (predicate(state)) return Promise.resolve(state);
return new Promise((resolve, reject) => {
const startedAt = Date.now();
const tick = () => {
if (predicate(state)) {
resolve(state);
return;
}
if (Date.now() - startedAt > timeoutMs) {
reject(new Error("timed out waiting for store state"));
return;
}
setTimeout(tick, intervalMs);
};
setTimeout(tick, intervalMs);
});
};
const dispatch = (action) => {
switch (action.type) {
case "BOOT_READY":
setState({ bootReady: true });
break;
case "ATTACH_KINEMATICS_RUNTIME":
{
const runtime = action.runtime || null;
const readiness = runtime?.readiness ? runtime.readiness() : null;
const runtimeDescriptor = runtime?.loaded
? {
apiName: runtime.apiName,
moduleId: runtime.moduleId,
wasmFile: runtime.wasmFile,
supportedModules: runtime.supportedModules,
loaded: runtime.loaded,
sourceMode: runtime.sourceMode,
semanticBoundary: runtime.semanticBoundary,
switchkinsType: runtime.switchkinsType,
switchRc: runtime.switchRc,
}
: null;
const adapter = createLinuxCncBoundaryAdapter({
profile: state.profile,
runtime: {
kinematicsWasm: runtimeDescriptor,
interpreterWasm: createInterpreterDescriptor(state.interpreterRuntime),
},
});
setState({
kinematicsRuntime: runtime,
kinematicsRuntimeReadiness: readiness,
kinematicsExecutionContext: runtime?.executionContext || (runtime?.loaded ? "direct" : "none"),
linuxCncBoundaryAdapter: adapter,
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
desiredFrameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
operatorMessage: runtime?.loaded
? `LinuxCNC kinematics ${runtime.moduleId} ready`
: "LinuxCNC kinematics runtime missing",
});
}
break;
case "ATTACH_INI_CONFIG":
{
const baseProfile = getFiveAxisProfile(action.profileId || state.machineProfile);
const profile = applyIniConfigToProfile(baseProfile, action.iniConfig);
const adapter = createLinuxCncBoundaryAdapter({
profile,
runtime: {
kinematicsWasm: createKinematicsDescriptor(state.kinematicsRuntime),
interpreterWasm: createInterpreterDescriptor(state.interpreterRuntime),
},
});
setState({
machineProfile: profile.id,
profile,
kinsType: normalizeKinsTypeForProfile(state.kinsType, profile),
axisPose: clampAxisPoseToProfile(state.axisPose, profile),
linuxCncIniConfig: action.iniConfig,
iniConfigReadiness: createIniConfigReadiness(action.iniConfig),
linuxCncBoundaryAdapter: adapter,
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
operatorMessage: `LinuxCNC INI loaded ${action.iniConfig.path}`,
});
}
break;
case "INI_CONFIG_FAILED":
setState({
iniConfigReadiness: {
apiName: "web-rtcp-5axis-ini-config-readiness",
loaded: false,
ready: false,
path: action.path || state.profile.iniPath,
missing: [action.error],
},
operatorMessage: `LinuxCNC INI error: ${action.error}`,
});
break;
case "SET_PROFILE":
{
const profile = getFiveAxisProfile(action.profileId);
const defaultKinsType = defaultKinsTypeForProfile(profile);
const adapter = createLinuxCncBoundaryAdapter({
profile,
runtime: {
kinematicsWasm: null,
interpreterWasm: createInterpreterDescriptor(state.interpreterRuntime),
},
});
setState({
machineProfile: profile.id,
profile,
activeProgram: profile.samplePrograms[0] || state.activeProgram,
kinsType: defaultKinsType,
rtcpState: rtcpStateFromKinsType(defaultKinsType),
kinematicsRuntime: null,
kinematicsRuntimeReadiness: null,
kinematicsExecutionContext: "none",
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: state.toolPreview?.length,
}),
...createInitialControlledUserMState(),
linuxCncIniConfig: null,
iniConfigReadiness: initialState.iniConfigReadiness,
linuxCncBoundaryAdapter: adapter,
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
sessionPersistence: {
...state.sessionPersistence,
status: "profile-switched",
lastError: null,
},
operatorMessage: `profile ${profile.id}`,
});
}
break;
case "ATTACH_INTERPRETER_RUNTIME":
{
const runtime = action.runtime || null;
const readiness = runtime?.readiness ? runtime.readiness() : null;
const adapter = createLinuxCncBoundaryAdapter({
profile: state.profile,
runtime: {
kinematicsWasm: createKinematicsDescriptor(state.kinematicsRuntime),
interpreterWasm: createInterpreterDescriptor(runtime),
},
});
setState({
interpreterRuntime: runtime,
interpreterRuntimeReadiness: readiness,
linuxCncBoundaryAdapter: adapter,
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
operatorMessage: runtime?.loaded
? "LinuxCNC interpreter ready"
: "LinuxCNC interpreter runtime missing",
});
}
break;
case "ATTACH_TASK_HAL_RUNTIME":
{
const runtime = action.runtime || null;
const maybeReadiness = action.readiness || (runtime?.readiness ? runtime.readiness() : null);
const readiness = typeof maybeReadiness?.then === "function"
? {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
loaded: Boolean(runtime?.loaded),
taskRuntimeReady: false,
motionRuntimeReady: false,
halRuntimeReady: false,
pending: true,
}
: maybeReadiness;
setState({
taskHalRuntime: runtime,
taskHalRuntimeReadiness: readiness,
taskHalFallbackReason: runtime?.loaded ? null : "LinuxCNC task/HAL runtime missing",
operatorMessage: runtime?.loaded
? "LinuxCNC task/HAL runtime ready"
: "LinuxCNC task/HAL runtime missing",
});
if (runtime?.loaded && state.machineFileStaging?.status === "staged") {
initializeTaskHalSession().catch(() => {});
}
}
break;
case "TASK_HAL_RUNTIME_FAILED":
setState({
taskHalRuntime: null,
taskHalRuntimeReadiness: {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
loaded: false,
taskRuntimeReady: false,
motionRuntimeReady: false,
halRuntimeReady: false,
nativeTaskReady: false,
nativeHalSyncReady: false,
error: action.error,
},
taskHalFallbackReason: action.error,
operatorMessage: `LinuxCNC task/HAL runtime blocked: ${action.error}`,
});
break;
case "RUN_INTERPRETER_PROGRAM":
if (!state.interpreterRuntime?.loaded) {
setState({
programExecutionSourceMode: "fixture-line-playback",
operatorMessage: "LinuxCNC interpreter unavailable; using fixture line playback",
});
break;
}
{
const programText = state.programLines.join("\n");
const sequence = state.interpreterExecutionSequence + 1;
setState({
interpreterExecutionPending: true,
interpreterExecutionSequence: sequence,
operatorMessage: "LinuxCNC interpreter running program",
});
try {
Promise.resolve(state.interpreterRuntime.runProgram(programText))
.then((execution) => {
dispatch({
type: "INTERPRETER_PROGRAM_COMPLETE",
sequence,
execution,
});
})
.catch((error) => {
dispatch({
type: "INTERPRETER_PROGRAM_FAILED",
sequence,
error: error instanceof Error ? error.message : String(error),
});
});
} catch (error) {
dispatch({
type: "INTERPRETER_PROGRAM_FAILED",
sequence,
error: error instanceof Error ? error.message : String(error),
});
}
}
break;
case "INTERPRETER_PROGRAM_COMPLETE":
if (action.sequence !== state.interpreterExecutionSequence) {
break;
}
{
const execution = action.execution;
const timing = buildTimingForState(state, execution);
const firstTiming = timingAtMotionIndex(timing, 0);
const firstMotion = execution.motion[0] || null;
const firstKinsType = kinsTypeFromProgramMotion(state, firstMotion) || state.kinsType;
const firstFeedback = createInitialProgramRuntimeFeedback({
state,
timing,
motion: firstMotion,
timingSnapshot: firstTiming,
});
const initialUiPatch = applyProgramPlaybackUiPatch({
...state,
programExecution: execution,
programExecutionTiming: timing,
programExecutionSampleIndex: 0,
programExecutionMotionIndex: 0,
activeLine: firstMotion?.line || state.programStartLine,
axisPose: axisPoseFromCanonicalMotion(firstMotion, state.axisPose),
kinsType: firstKinsType,
rtcpState: rtcpStateFromKinsType(firstKinsType),
programRuntimeFeedback: firstFeedback,
}, {
activeLine: firstMotion?.line || state.programStartLine,
axisPose: axisPoseFromCanonicalMotion(firstMotion, state.axisPose),
kinsType: firstKinsType,
rtcpState: rtcpStateFromKinsType(firstKinsType),
motionIndex: 0,
sampleIndex: 0,
runtimeFeedback: firstFeedback,
});
setState({
programExecution: execution,
programExecutionTiming: timing,
programExecutionSourceMode: execution.sourceMode,
machineFileExecution: execution.machineFilePlan ? execution : state.machineFileExecution,
...initialUiPatch,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, firstFeedback, {
status: "ready",
source: execution.sourceMode,
}),
programElapsedSeconds: firstTiming.elapsedSeconds,
programRemainingSeconds: firstTiming.remainingSeconds,
interpreterExecutionPending: false,
preview: {
...state.preview,
pathPoints: state.programAxisPreviewPath?.sampleCount || Math.max(execution.summary.motionEventCount, 1),
},
feed: {
...state.feed,
currentVelocity: Number.isFinite(firstFeedback?.currentVelocityMmPerMin)
? firstFeedback.currentVelocityMmPerMin
: state.feed.currentVelocity,
},
operatorMessage: execution.summary.switchkinsEventCount > 0
? `LinuxCNC interpreter motion events ${execution.summary.motionEventCount}, switchkins ${execution.summary.switchkinsCodes.join("/")}`
: `LinuxCNC interpreter motion events ${execution.summary.motionEventCount}`,
});
}
break;
case "INTERPRETER_PROGRAM_FAILED":
if (action.sequence !== state.interpreterExecutionSequence) {
break;
}
setState({
programExecution: null,
programExecutionTiming: null,
programElapsedSeconds: 0,
programRemainingSeconds: 0,
programExecutionSourceMode: "fixture-line-playback",
programExecutionSampleIndex: 0,
programRuntimeFeedback: null,
programLineExecution: {},
interpreterExecutionPending: false,
preview: {
...state.preview,
pathPoints: state.programAxisPreviewPath?.sampleCount || state.preview.pathPoints,
},
operatorMessage: `LinuxCNC interpreter blocked: ${action.error}`,
});
break;
case "RUN_MACHINE_FILE_PROGRAM":
if (!state.interpreterRuntime?.loaded || typeof state.interpreterRuntime.runMachineFileProgram !== "function") {
setState({
operatorMessage: "machine-file run blocked: LinuxCNC interpreter machine-file runtime unavailable",
});
break;
}
if (!state.machineFileStaging?.plan || !state.machineFileStaging?.save) {
setState({
operatorMessage: "machine-file run blocked: machine files not staged",
});
break;
}
if (!state.machineFileStaging.selectedGcodeSourceRel) {
setState({
operatorMessage: "machine-file run blocked: select a LinuxCNC source-directory 5-axis G-code program",
});
break;
}
{
const sequence = state.interpreterExecutionSequence + 1;
setState({
interpreterExecutionPending: true,
interpreterExecutionSequence: sequence,
operatorMessage: "LinuxCNC machine-file remap run starting",
});
try {
Promise.resolve(state.interpreterRuntime.runMachineFileProgram({
plan: selectMachineFileProgramForState(state),
files: state.machineFileStaging.save.files,
executionMode: "fiveAxisRemap",
}))
.then((execution) => {
dispatch({ type: "INTERPRETER_PROGRAM_COMPLETE", sequence, execution });
})
.catch((error) => {
dispatch({
type: "INTERPRETER_PROGRAM_FAILED",
sequence,
error: error instanceof Error ? error.message : String(error),
});
});
} catch (error) {
dispatch({
type: "INTERPRETER_PROGRAM_FAILED",
sequence,
error: error instanceof Error ? error.message : String(error),
});
}
}
break;
case "SESSION_SAVE_STARTED":
setState({
sessionPersistence: {
...state.sessionPersistence,
status: "saving",
lastError: null,
},
operatorMessage: "saving 5-axis session",
});
break;
case "SESSION_SAVE_COMPLETE":
setState({
sessionPersistence: {
...state.sessionPersistence,
status: "saved",
path: action.path,
storageMode: action.storageMode || null,
storageCapability: action.storageCapability || null,
savedAt: action.savedAt,
lastError: null,
},
operatorMessage: `5-axis session saved ${action.path} (${action.storageMode || "unknown"})`,
});
break;
case "SESSION_RESTORE_STARTED":
setState({
sessionPersistence: {
...state.sessionPersistence,
status: "restoring",
lastError: null,
},
operatorMessage: "restoring 5-axis session",
});
break;
case "SESSION_RESTORE_COMPLETE":
{
const restoredProfile = getFiveAxisProfile(action.restoredState.machineProfile);
const adapter = createLinuxCncBoundaryAdapter({
profile: restoredProfile,
runtime: {
kinematicsWasm: createKinematicsDescriptor(state.kinematicsRuntime),
interpreterWasm: createInterpreterDescriptor(state.interpreterRuntime),
},
});
setState({
...action.restoredState,
profile: restoredProfile,
linuxCncIniConfig: state.linuxCncIniConfig,
iniConfigReadiness: state.iniConfigReadiness,
kinematicsRuntime: state.kinematicsRuntime,
kinematicsRuntimeReadiness: state.kinematicsRuntimeReadiness,
kinematicsExecutionContext: state.kinematicsExecutionContext,
interpreterRuntime: state.interpreterRuntime,
interpreterRuntimeReadiness: state.interpreterRuntimeReadiness,
taskHalRuntime: state.taskHalRuntime,
taskHalRuntimeReadiness: state.taskHalRuntimeReadiness,
taskHalStatus: state.taskHalStatus,
taskHalSession: state.taskHalSession,
machineFileStaging: state.machineFileStaging,
machineFileExecution: state.machineFileExecution,
linuxCncBoundaryAdapter: adapter,
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
sessionPersistence: {
...state.sessionPersistence,
status: "restored",
path: action.path,
storageMode: action.storageMode || null,
storageCapability: action.storageCapability || null,
restoredAt: action.restoredAt,
lastError: null,
},
operatorMessage: `5-axis session restored ${action.path} (${action.storageMode || "unknown"})`,
});
}
break;
case "SESSION_PERSISTENCE_FAILED":
setState({
sessionPersistence: {
...state.sessionPersistence,
status: "error",
lastError: action.error,
},
operatorMessage: `5-axis session error: ${action.error}`,
});
break;
case "MACHINE_FILE_STAGING_STARTED":
setState({
machineFileStaging: {
...state.machineFileStaging,
status: "staging",
profileId: state.machineProfile,
lastError: null,
},
operatorMessage: "staging LinuxCNC machine files",
});
break;
case "MACHINE_FILE_STAGING_COMPLETE":
setState({
machineFileStaging: {
...state.machineFileStaging,
status: "staged",
profileId: action.plan.profileId,
fileCount: action.save.fileCount,
opfsRoot: action.save.opfsRoot,
storageMode: action.save.storageMode || null,
storageCapability: action.save.storageCapability || null,
savedAt: action.save.savedAt,
lastError: null,
plan: action.plan,
save: action.save,
gcodeSources: listLinuxCncGcodeSources(action.save),
gcodeFiles: listProjectGcodeFiles(action.save),
selectedGcodeSourceRel: action.selectedGcodeSourceRel
|| action.plan.selectedProgramSourceRel
|| null,
},
...createToolDbStatePatchFromStagedFiles({
profile: state.profile,
save: action.save,
}),
operatorMessage: `LinuxCNC machine files staged ${action.save.fileCount}`,
});
if (state.taskHalRuntime?.loaded) {
initializeTaskHalSession().catch(() => {});
}
break;
case "LOAD_LINUXCNC_GCODE_SOURCE":
{
const sourceRel = action.sourceRel;
const selectedFile = state.machineFileStaging?.save?.files?.find((file) => file.sourceRel === sourceRel);
if (!selectedFile) {
setState({
operatorMessage: `LinuxCNC G-code source not staged: ${sourceRel}`,
});
break;
}
const selectedPlan = selectMachineFileProgram(
state.machineFileStaging.plan,
state.machineFileStaging.save,
sourceRel,
);
const loadedProgram = buildLoadedProgram({
filename: selectedFile.sourceRel,
content: selectedFile.text,
programSource: "linuxcnc-vendored-5axis-gcode",
sourceRel: selectedFile.sourceRel,
wasmPath: selectedFile.wasmPath,
});
const toolUserPatch = createProgramToolUserSimulationPatch({
state,
programText: selectedFile.text,
sourceRel: selectedFile.sourceRel,
});
const axisPreviewPath = buildProgramAxisPathFromProgram({
filename: selectedFile.sourceRel,
sourceRel: selectedFile.sourceRel,
content: selectedFile.text,
tool: currentPathTool({
...state,
toolRuntimeState: toolUserPatch.toolRuntimeState,
}),
});
setState({
...loadedProgram,
...toolUserPatch,
machineFileStaging: {
...state.machineFileStaging,
plan: selectedPlan,
selectedGcodeSourceRel: sourceRel,
},
machine: {
...state.machine,
mode: state.machine.mode,
},
axisPose: initialAxisPose,
runState: "idle",
programAxisPreviewPath: axisPreviewPath,
programRuntimeFeedback: null,
programUiExecution: null,
programLineExecution: {},
preview: {
...state.preview,
pathPoints: axisPreviewPath?.sampleCount || Math.max(loadedProgram.programLines.length, 1),
},
operatorMessage: `loaded LinuxCNC 5-axis source ${selectedFile.sourceRel}`,
});
if (state.taskHalRuntime?.loaded) {
initializeTaskHalSession({ openProgram: true }).catch(() => {});
}
if (state.interpreterRuntime?.loaded) {
dispatch({ type: "RUN_INTERPRETER_PROGRAM" });
}
}
break;
case "RUN_CONTROLLED_USER_M":
{
const result = runControlledUserM(
state.controlledUserMSimulation || createControlledUserMSimulation(),
action.code,
{
profile: state.profile,
sourceRel: action.sourceRel || state.programSourceRel || null,
line: action.line || null,
},
);
setState({
controlledUserMSimulation: result.simulation,
controlledUserMReadiness: createControlledUserMReadiness(result.simulation),
...result.statePatch,
operatorMessage: result.event.allowed
? `controlled user-M ${result.event.code} simulated`
: `controlled user-M ${result.event.code} blocked`,
});
}
break;
case "TASK_HAL_SESSION_READY":
if (action.session?.programSourceRel && action.session.programSourceRel !== state.machineFileStaging?.selectedGcodeSourceRel) {
break;
}
setState({
taskHalSession: action.session,
taskHalFallbackReason: null,
operatorMessage: `LinuxCNC task/HAL session ready ${action.session.programPath || "-"}`,
});
break;
case "TASK_HAL_STATUS_APPLIED":
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage, {
loopSequence: action.loopSequence,
preserveMachine: action.preserveMachine,
preserveAxisPose: action.preserveAxisPose,
}));
break;
case "TASK_HAL_STATUS_LOOP_STARTED":
setState({
taskHalStatusLoop: {
...createTaskHalStatusLoopState({
active: true,
sequence: action.sequence,
profileId: action.profileId,
iniPath: action.iniPath,
kinematicsModuleId: action.kinematicsModuleId,
batchSize: action.batchSize,
intervalMs: action.intervalMs,
taskPeriodNs: action.taskPeriodNs,
servoPeriodNs: action.servoPeriodNs,
}),
},
programRuntimeFeedbackHistory: [],
operatorMessage: action.operatorMessage || "task/HAL status loop running",
});
break;
case "TASK_HAL_STATUS_LOOP_STOPPED":
setState({
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
sequence: Number(state.taskHalStatusLoop?.sequence || 0) + 1,
stopReason: action.reason || "stopped",
lastError: action.error || null,
},
operatorMessage: action.operatorMessage || state.operatorMessage,
});
break;
case "TASK_HAL_PROGRAM_STOPPED":
setState(createStoppedProgramStatePatch(state, {
reason: action.reason || "stopped",
operatorMessage: action.operatorMessage || "task/HAL program stopped",
}));
break;
case "TASK_HAL_COMMAND_FAILED":
setState({
taskHalFallbackReason: action.error,
taskHalExecutionPending: false,
taskHalPauseLock: null,
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
lastError: action.error,
stopReason: "error",
},
pendingJogCommand: null,
operatorMessage: `task/HAL fallback: ${action.error}`,
});
break;
case "MACHINE_FILE_STAGING_FAILED":
setState({
machineFileStaging: {
...state.machineFileStaging,
status: "error",
profileId: state.machineProfile,
lastError: action.error,
},
operatorMessage: `machine file staging error: ${action.error}`,
});
break;
case "SAVE_SESSION_REQUEST":
saveSession().catch(() => {});
break;
case "RESTORE_SESSION_REQUEST":
restoreSession().catch(() => {});
break;
case "STAGE_MACHINE_FILES_REQUEST":
stageMachineFiles(action.options || {}).catch(() => {});
break;
case "RUN_FULL_BOUNDARY_AUDIT_REQUEST":
runFullBoundaryAudit(action.options || {}).catch(() => {});
break;
case "RUN_READY":
runReadySequence().catch((error) => {
dispatch({
type: "TASK_HAL_COMMAND_FAILED",
error: error instanceof Error ? error.message : String(error),
});
});
break;
case "RUN_FROM_OPERATOR":
return operatorRunSequence().catch((error) => {
dispatch({
type: "TASK_HAL_COMMAND_FAILED",
error: error instanceof Error ? error.message : String(error),
});
});
case "SET_FRAME_SOURCE":
setState({
sourceMode: action.sourceMode,
frameSourceMode: action.sourceMode,
desiredFrameSourceMode: action.sourceMode,
operatorMessage: `frame source ${action.sourceMode}`,
});
break;
case "REFRESH_KINEMATICS_FRAME":
return refreshAsyncKinematicsFrame({ operatorMessage: "LinuxCNC kinematics frame refreshed" });
break;
case "GMOCAPY_HARDWARE_BUTTON":
{
if (action.value === false || action.risingEdge === false) {
setState({ operatorMessage: "gmoccapy hardware button falling edge ignored" });
break;
}
const button = resolveGmoccapyHardwareButton(action);
if (!button) {
setState({ operatorMessage: `gmoccapy hardware button unmapped: ${action.pin || `${action.location}:${action.index}`}` });
break;
}
if (!button.webDispatch) {
setState({ operatorMessage: `gmoccapy hardware button diagnostic-only: ${button.pin}` });
break;
}
dispatch(button.webDispatch);
}
break;
case "GMOCAPY_HAL_PIN":
{
const result = applyGmoccapyHalPinPatch(state, action, gmoccapyHalModel);
if (!result.applied) {
setState({ operatorMessage: result.operatorMessage });
break;
}
setState(result.patch);
}
break;
case "GMOCAPY_NATIVE_PAGE":
{
const patch = applyGmoccapyNativePagePatch(state, action, gmoccapyHalModel);
setState(patch);
}
break;
case "GMOCAPY_PAGE_ACTION":
{
const patch = applyGmoccapyPageActionPatch(state, action, gmoccapyHalModel);
setState(patch);
}
break;
case "GMOCAPY_RUN_MACRO":
{
const result = applyGmoccapyMacroPatch(state, action, gmoccapyHalModel);
if (!result.applied) {
setState({ operatorMessage: result.operatorMessage });
break;
}
setState(result.patch);
}
break;
case "GMOCAPY_TOOL_EDITOR_ACTION":
{
const patch = applyGmoccapyToolEditorPatch(state, action, gmoccapyHalModel);
setState(patch);
}
break;
case "TOGGLE_POWER":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const turningOn = state.machine.taskState === "estop-reset";
if (state.taskHalRuntime?.loaded) {
const nextMachine = createPowerToggleMachinePatch(state.machine, turningOn);
setState({
machine: nextMachine,
runState: turningOn ? "idle" : "powered-off",
kinsType: turningOn ? state.kinsType : "identity",
rtcpState: turningOn ? state.rtcpState : "off",
feed: turningOn ? state.feed : { ...state.feed, currentVelocity: 0 },
coolant: turningOn ? state.coolant : { ...state.coolant, flood: false, mist: false },
spindle: turningOn ? state.spindle : stoppedSpindleState(state.spindle),
operatorMessage: turningOn ? "task/HAL machine power on" : "task/HAL machine power off",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: turningOn ? "ON" : "OFF" },
], { operatorMessage: turningOn ? "task/HAL machine power on" : "task/HAL machine power off" }).catch(() => {});
break;
}
const nextMachine = createPowerToggleMachinePatch(state.machine, turningOn);
setState({
machine: nextMachine,
runState: turningOn ? "idle" : "powered-off",
kinsType: turningOn ? state.kinsType : "identity",
rtcpState: turningOn ? state.rtcpState : "off",
feed: turningOn ? state.feed : { ...state.feed, currentVelocity: 0 },
coolant: turningOn ? state.coolant : { ...state.coolant, flood: false, mist: false },
spindle: turningOn ? state.spindle : stoppedSpindleState(state.spindle),
operatorMessage: turningOn ? "machine power on" : "machine power off",
});
}
break;
case "ESTOP":
setState({
machine: {
...state.machine,
powerOn: false,
estopActive: true,
motionEnabled: false,
taskState: "estop",
manualPanel: "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: "estopped",
taskHalPauseLock: null,
kinsType: "identity",
rtcpState: "off",
feed: {
...state.feed,
currentVelocity: 0,
},
coolant: {
...state.coolant,
flood: false,
mist: false,
},
spindle: {
...stoppedSpindleState(state.spindle),
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "emergency stop active",
});
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ESTOP" },
], {
operatorMessage: "task/HAL emergency stop active",
preserveMachine: {
...state.machine,
powerOn: false,
estopActive: true,
motionEnabled: false,
taskState: "estop",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
}).catch(() => {});
}
break;
case "RESET":
setState({
machine: {
...state.machine,
powerOn: false,
estopActive: false,
motionEnabled: false,
taskState: "estop-reset",
manualPanel: "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
resetCount: state.machine.resetCount + 1,
},
runState: "idle",
taskHalPauseLock: null,
kinsType: "identity",
rtcpState: "off",
coolant: {
...state.coolant,
flood: false,
mist: false,
},
spindle: {
...stoppedSpindleState(state.spindle),
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "estop reset; machine off",
});
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ESTOP_RESET" },
], {
operatorMessage: "task/HAL estop reset; machine off",
preserveMachine: {
...state.machine,
powerOn: false,
estopActive: false,
motionEnabled: false,
taskState: "estop-reset",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
}).catch(() => {});
}
break;
case "SET_MODE":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const requestedMode = String(action.mode || "");
const mode = normalizeLinuxCncTaskMode(requestedMode);
const manualPanel = requestedMode === "jog"
? "jog"
: mode === "manual"
? "manual"
: null;
if (state.taskHalRuntime?.loaded) {
const manualModePatch = mode === "manual"
? {
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
}
: {};
const requestedMachine = {
...state.machine,
mode,
manualPanel,
...manualModePatch,
};
setState({
machine: requestedMachine,
operatorMessage: `task/HAL mode ${mode} requested`,
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_MODE", mode: mode.toUpperCase() },
], {
operatorMessage: `task/HAL mode ${mode}`,
preserveMachine: requestedMachine,
}).catch(() => {});
break;
}
setState({
machine: {
...state.machine,
mode,
manualPanel,
interpState: mode === "manual" ? "idle" : state.machine.interpState,
interpResumeState: mode === "manual" ? "idle" : state.machine.interpResumeState,
taskPaused: mode === "manual" ? false : state.machine.taskPaused,
motionPaused: mode === "manual" ? false : state.machine.motionPaused,
singleStepping: mode === "manual" ? false : state.machine.singleStepping,
motionStepping: mode === "manual" ? false : state.machine.motionStepping,
},
runState: mode === "manual" && state.runState === "running" ? "stopped" : state.runState,
operatorMessage: `mode ${mode}`,
});
}
break;
case "SET_MDI_COMMAND":
setState({
machine: {
...state.machine,
mdiCommand: String(action.command ?? ""),
},
operatorMessage: "MDI command staged",
});
break;
case "SET_ACTIVE_JOINT":
{
const joint = Math.min(Math.max(Number(action.joint) || 0, 0), Math.max((state.profile?.joints?.length || 5) - 1, 0));
const axis = (state.profile?.jointConfig?.[joint]?.axis || ["X", "Y", "Z", "B", "C"][joint] || "X").toLowerCase();
setState({
machine: {
...state.machine,
selectedJoint: joint,
jogAxis: axis,
},
operatorMessage: `joint ${joint} selected`,
});
}
break;
case "SET_JOG_INCREMENT":
{
const increment = Math.max(Number(action.increment) || 0, 0);
setState({
machine: {
...state.machine,
jogIncrement: increment,
},
gmoccapyGui: {
...state.gmoccapyGui,
jogIncrementLabel: increment === 0 ? "Continuous" : increment.toFixed(4),
jogIncrementOutput: increment,
},
operatorMessage: increment === 0 ? "jog continuous" : `jog increment ${increment}`,
});
}
break;
case "JOG":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const axis = action.axis || state.machine.jogAxis;
const direction = Number(action.direction || 1);
const increment = Number(action.increment || state.machine.jogIncrement);
if (state.taskHalRuntime?.loaded) {
const nextAxisPose = clampAxisPoseToProfile({
...state.axisPose,
[axis]: Number(state.axisPose[axis] || 0) + direction * increment,
}, state.profile);
const pendingJogCommand = {
axis,
direction,
increment,
basePose: { ...state.axisPose },
createdAtLine: state.activeLine,
};
setState({
machine: {
...state.machine,
mode: "manual",
manualPanel: "jog",
jogAxis: axis,
jogIncrement: increment,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
axisPose: nextAxisPose,
runState: "jogging",
pendingJogCommand,
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
});
runTaskHalCommandSequence([
{
type: "EMC_JOG_INCR",
axis: axis.toUpperCase(),
distance: direction * increment,
velocity: Number(action.velocity || 60),
},
], {
pendingJogCommand,
preserveAxisPose: nextAxisPose,
preserveMachine: {
...state.machine,
mode: "manual",
manualPanel: "jog",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
}).catch(() => {});
break;
}
setState({
machine: {
...state.machine,
mode: "manual",
manualPanel: "jog",
jogAxis: axis,
jogIncrement: increment,
},
axisPose: {
...state.axisPose,
[axis]: Number(state.axisPose[axis] || 0) + direction * increment,
},
runState: "jogging",
operatorMessage: `jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
});
}
break;
case "RUN_MDI":
{
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
const gate = gateLinuxCncTaskAction(state, { ...action, command });
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const preserveManualTouchOff = action.manualTouchOff === true && state.machine.mode === "manual";
if (state.taskHalRuntime?.loaded) {
const mdiResult = executeMdiCommand(state, command);
const mdiPatch = preserveManualTouchOff
? createManualTouchOffMdiPatch(state, mdiResult.patch, command)
: mdiResult.patch;
setState(mdiPatch);
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_MODE", mode: "MDI" },
{ type: "EMC_TASK_PLAN_EXECUTE", mdi: command },
], {
operatorMessage: `task/HAL MDI ${command}`,
preserveAxisPose: mdiPatch.axisPose,
preserveMachine: mdiPatch.machine,
}).catch(() => {});
break;
}
const mdiResult = executeMdiCommand(state, command);
setState(preserveManualTouchOff
? createManualTouchOffMdiPatch(state, mdiResult.patch, command)
: mdiResult.patch);
}
break;
case "LOAD_PROGRAM":
{
const loadedProgram = buildLoadedProgram(action);
const toolCommands = extractToolCommandSequenceFromProgram(loadedProgram.programLines.join("\n"));
const toolDbSimulation = state.toolDbSimulation && toolCommands.length > 0
? applyToolCommandSequence(state.toolDbSimulation, toolCommands)
: state.toolDbSimulation;
const toolRuntimeState = createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
});
const axisPreviewPath = buildProgramAxisPathFromProgram({
filename: loadedProgram.activeProgram,
sourceRel: loadedProgram.programSourceRel,
content: action.content,
tool: currentPathTool({
...state,
toolRuntimeState,
}),
});
setState({
...loadedProgram,
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState,
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
axisPose: initialAxisPose,
runState: "idle",
programAxisPreviewPath: axisPreviewPath,
programRuntimeFeedback: null,
programLineExecution: {},
preview: {
...state.preview,
pathPoints: axisPreviewPath?.sampleCount || Math.max(loadedProgram.programLines.length, 1),
},
operatorMessage: `loaded ${loadedProgram.activeProgram}`,
});
if (state.interpreterRuntime?.loaded) {
dispatch({ type: "RUN_INTERPRETER_PROGRAM" });
}
}
break;
case "RUN":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
if (state.taskHalRuntime?.loaded) {
const preconditions = validateRunPreconditions(state, { requireTaskHalSession: false });
if (!preconditions.ok) {
setState({ operatorMessage: preconditions.operatorMessage });
break;
}
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
taskHalPauseLock: null,
operatorMessage: "task/HAL program run requested; waiting for status",
});
runValidatedTaskHalProgramRun().catch(() => {});
break;
}
const playback = nextProgramRuntimeSamplePlayback(state, 5);
const playbackPatch = applyProgramPlaybackUiPatch(state, {
activeLine: playback.activeLine,
axisPose: playback.axisPose,
kinsType: playback.kinsType,
rtcpState: playback.rtcpState,
motionIndex: playback.motionIndex,
sampleIndex: playback.sampleIndex,
runtimeFeedback: playback.runtimeFeedback,
});
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
interpState: playback.complete ? "idle" : "reading",
interpResumeState: playback.complete ? "idle" : "reading",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: playback.complete ? "complete" : "running",
taskHalPauseLock: null,
...playbackPatch,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, playback.runtimeFeedback, {
status: playback.complete ? "done" : "running",
source: playback.runtimeFeedback?.sourceMode,
}),
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
feed: {
...state.feed,
currentVelocity: playback.timing.currentVelocity,
},
operatorMessage: `executing line ${playback.activeLine}`,
});
}
break;
case "STOP":
case "ABORT":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
if (state.taskHalRuntime?.loaded) {
stopTaskHalStatusLoop(action.type === "ABORT" ? "aborted" : "stopped", {
operatorMessage: action.type === "ABORT" ? "task/HAL abort requested" : "task/HAL stop requested",
});
setState(createStoppedProgramStatePatch(state, {
reason: action.type === "ABORT" ? "aborted" : "stopped",
operatorMessage: action.type === "ABORT" ? "task abort requested" : "program stop requested",
}));
runTaskHalCommandSequence([
{ type: "EMC_TASK_ABORT" },
], {
operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped",
preserveMachine: {
...state.machine,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
})
.then(() => {
dispatch({
type: "TASK_HAL_PROGRAM_STOPPED",
reason: action.type === "ABORT" ? "aborted" : "stopped",
operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped",
});
})
.catch(() => {});
break;
}
setState({
machine: {
...state.machine,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: "stopped",
taskHalPauseLock: null,
feed: {
...state.feed,
currentVelocity: 0,
},
operatorMessage: action.type === "ABORT" ? "task abort complete" : "program stopped",
});
}
break;
case "SET_SPINDLE_DIRECTION":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const direction = normalizeSpindleDirection(action.direction);
const spindleEnabled = direction !== "stop";
const spindleActualRpm = spindleEnabled
? Number(state.spindle.rpm || 0) * (Number(state.spindle.override || 100) / 100)
: 0;
setState({
spindle: {
...state.spindle,
enabled: spindleEnabled,
direction,
halPins: {
on: spindleEnabled ? 1 : 0,
forward: direction === "forward" ? 1 : 0,
reverse: direction === "reverse" ? 1 : 0,
speedOut: spindleActualRpm,
atSpeed: spindleEnabled ? 1 : 0,
},
},
operatorMessage: direction === "stop" ? "spindle stopped" : `spindle ${direction}`,
});
}
break;
case "PAUSE":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
if (state.taskHalRuntime?.loaded) {
stopTaskHalStatusLoop("paused", { operatorMessage: "task/HAL pause requested" });
const pausedMachine = {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
motionPaused: true,
singleStepping: false,
motionStepping: false,
};
const pauseLock = createTaskHalPauseLock(state, "task-hal-plan-pause");
setState({
machine: pausedMachine,
runState: "paused",
taskHalPauseLock: pauseLock,
feed: {
...state.feed,
currentVelocity: 0,
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "task/HAL pause requested",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_PAUSE" },
], {
operatorMessage: "task/HAL program paused",
}).then((status) => {
if (isTaskHalStatusPaused(status)) {
startTaskHalStatusLoop({
batchSize: 1,
operatorMessage: "task/HAL paused status monitor running",
});
}
}).catch(() => {});
break;
}
const pauseLock = createTaskHalPauseLock(state, "fixture-plan-pause");
setState({
machine: {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState,
interpState: "paused",
taskPaused: true,
motionPaused: true,
singleStepping: false,
motionStepping: false,
},
runState: "paused",
taskHalPauseLock: pauseLock,
feed: {
...state.feed,
currentVelocity: 0,
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "program paused",
});
}
break;
case "PAUSE_RESUME":
{
const interpState = state.machine?.interpState || state.linuxCncTaskPolicy?.interpState || "idle";
const taskMode = normalizeLinuxCncTaskMode(state.machine?.mode);
if (taskMode !== "auto" && taskMode !== "mdi") {
setState({ operatorMessage: "pause blocked: task mode must be auto or MDI" });
break;
}
if (state.machine?.motionPaused === true || state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
dispatch({ type: "RESUME", source: "pauseresume" });
break;
}
if (interpState !== "idle") {
dispatch({ type: "PAUSE", source: "pauseresume" });
break;
}
setState({ operatorMessage: "pause ignored: interpreter is idle" });
}
break;
case "RESUME":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
if (state.taskHalRuntime?.loaded) {
const resumeState = state.machine.interpResumeState === "idle" || state.machine.interpResumeState === "paused"
? "reading"
: state.machine.interpResumeState || "reading";
const resumedMachine = {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
};
setState({
machine: resumedMachine,
runState: resumeState === "reading" || resumeState === "waiting" ? "running" : "idle",
taskHalPauseLock: null,
operatorMessage: "task/HAL resume requested",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_RESUME" },
], {
operatorMessage: "task/HAL program resumed",
}).then((status) => {
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
operatorMessage: "task/HAL status loop resumed",
});
}
}).catch(() => {});
break;
}
const resumeState = state.machine.interpResumeState === "idle"
? "reading"
: state.machine.interpResumeState;
setState({
machine: {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: resumeState === "reading" || resumeState === "waiting" ? "running" : "idle",
taskHalPauseLock: null,
operatorMessage: "program resumed",
});
}
break;
case "STEP":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
if (state.taskHalRuntime?.loaded) {
stopTaskHalStatusLoop("step", { operatorMessage: "task/HAL step requested" });
const playback = nextProgramRuntimeSamplePlayback(state, 1);
const playbackPatch = applyProgramPlaybackUiPatch(state, {
activeLine: playback.activeLine,
axisPose: playback.axisPose,
kinsType: playback.kinsType,
rtcpState: playback.rtcpState,
motionIndex: playback.motionIndex,
sampleIndex: playback.sampleIndex,
runtimeFeedback: playback.runtimeFeedback,
});
const steppedMachine = {
...state.machine,
mode: "auto",
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState || "reading"
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
motionPaused: true,
singleStepping: true,
motionStepping: true,
};
setState({
machine: steppedMachine,
runState: "stepping",
taskHalPauseLock: null,
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
feed: {
...state.feed,
currentVelocity: playback.timing.currentVelocity,
},
operatorMessage: `task/HAL step requested line ${playback.activeLine}`,
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_STEP" },
], {
taskCycles: 1,
operatorMessage: "task/HAL stepped one cycle",
preserveMachine: steppedMachine,
}).catch(() => {});
break;
}
const playback = nextProgramRuntimeSamplePlayback(state, 1);
const playbackPatch = applyProgramPlaybackUiPatch(state, {
activeLine: playback.activeLine,
axisPose: playback.axisPose,
kinsType: playback.kinsType,
rtcpState: playback.rtcpState,
motionIndex: playback.motionIndex,
sampleIndex: playback.sampleIndex,
runtimeFeedback: playback.runtimeFeedback,
});
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState,
interpState: "paused",
taskPaused: true,
motionPaused: true,
singleStepping: true,
motionStepping: true,
},
runState: "stepping",
taskHalPauseLock: null,
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
feed: {
...state.feed,
currentVelocity: playback.timing.currentVelocity,
},
operatorMessage: `stepped to line ${playback.activeLine}`,
});
}
break;
case "RUN_FRAME":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const playback = nextProgramRuntimeSamplePlayback(state, 5);
const playbackPatch = applyProgramPlaybackUiPatch(state, {
activeLine: playback.activeLine,
axisPose: playback.axisPose,
kinsType: playback.kinsType,
rtcpState: playback.rtcpState,
motionIndex: playback.motionIndex,
sampleIndex: playback.sampleIndex,
runtimeFeedback: playback.runtimeFeedback,
});
setState({
machine: {
...state.machine,
interpState: playback.complete ? "idle" : "reading",
interpResumeState: playback.complete ? "idle" : "reading",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: "running",
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
feed: {
...state.feed,
currentVelocity: playback.timing.currentVelocity,
},
});
}
break;
case "HOME":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const homeAxisPose = homeAxisPoseForState(state);
const homedFalse = createHomedArrayForState(state, false);
const homedTrue = createHomedArrayForState(state, true);
if (state.taskHalRuntime?.loaded) {
setState({
machine: {
...state.machine,
mode: "manual",
manualPanel: "manual",
allHomed: false,
homed: homedFalse,
homing: true,
homeState: "homing",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: "idle",
axisPose: homeAxisPose,
programRuntimeFeedback: null,
operatorMessage: "task/HAL home requested",
});
runTaskHalCommandSequence([
{ type: "EMC_JOINT_HOME", joint: -1 },
], {
operatorMessage: "task/HAL machine homed",
preserveAxisPose: homeAxisPose,
preserveMachine: {
manualPanel: "manual",
allHomed: true,
homed: homedTrue,
homing: false,
homeState: "homed",
},
}).catch(() => {});
break;
}
setState({
machine: {
...state.machine,
mode: "manual",
manualPanel: "manual",
allHomed: true,
homed: homedTrue,
homing: false,
homeState: "homed",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: "idle",
axisPose: homeAxisPose,
programRuntimeFeedback: null,
operatorMessage: "machine homed to fixture origin",
});
}
break;
case "UNHOME":
setState({
machine: {
...state.machine,
allHomed: false,
homed: createHomedArrayForState(state, false),
homing: false,
homeState: "unhomed",
},
operatorMessage: "machine unhomed",
});
break;
case "SET_RTCP":
if (action.enabled && !profileSupportsTcp(state.profile)) {
setState({
kinsType: "identity",
rtcpState: "off",
operatorMessage: `RTCP blocked: ${state.profile.id} is ${state.profile.kinematics} reference only`,
});
break;
}
{
const requestedKinsType = action.enabled
? tcpKinsTypeForProfile(state.profile)
: "identity";
const gate = gateKinsTypeChange(state, requestedKinsType);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
setState({
kinsType: requestedKinsType,
rtcpState: action.enabled ? "on" : "off",
});
}
break;
case "RESET_VIEW":
setState({
preview: {
...state.preview,
selectedView: "iso",
cameraRevision: (state.preview.cameraRevision ?? 0) + 1,
},
operatorMessage: "preview fit to program",
});
break;
case "CLEAR_PREVIEW":
setState({
preview: { ...state.preview, pathPoints: 0 },
operatorMessage: "preview path cleared",
});
break;
case "SET_VIEW":
setState({
preview: {
...state.preview,
selectedView: action.view,
cameraRevision: (state.preview.cameraRevision ?? 0) + 1,
},
operatorMessage: `preview view ${action.view}`,
});
break;
case "TOGGLE_FULLSCREEN":
setState({
preview: { ...state.preview, fullscreen: !state.preview.fullscreen },
operatorMessage: state.preview.fullscreen ? "fullscreen preview off" : "fullscreen preview on",
});
break;
case "SET_KINS_TYPE":
{
const gate = gateKinsTypeChange(state, action.kinsType);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
setState({
kinsType: action.kinsType,
rtcpState: action.kinsType.startsWith("tcp-") ? "on" : "off",
operatorMessage: `kinematics ${action.kinsType}`,
});
break;
}
case "ADJUST_OVERRIDE":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
setState({
feed: {
...state.feed,
[`${action.target}Override`]: clampPercent(
state.feed[`${action.target}Override`] + action.delta,
0,
200,
),
},
gmoccapyGui: {
...state.gmoccapyGui,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: `${action.target} override manual step`,
},
operatorMessage: `${action.target} override adjusted`,
});
}
break;
case "ADJUST_SPINDLE_OVERRIDE":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
setState({
spindle: {
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
halPins: spindleHalPinsForState({
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
}),
},
gmoccapyGui: {
...state.gmoccapyGui,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: "spindle override manual step",
},
operatorMessage: "spindle override adjusted",
});
}
break;
case "RESET_OVERRIDE":
setState(resetOverridePatchForTarget(state, action.target));
break;
case "SET_BLOCK_DELETE":
setState({
gmoccapyGui: {
...state.gmoccapyGui,
optionalBlocks: Boolean(action.enabled),
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: "block delete set from Web optional blocks button",
},
operatorMessage: `block delete ${action.enabled ? "on" : "off"}`,
});
break;
case "SET_OPTIONAL_STOP":
setState({
gmoccapyGui: {
...state.gmoccapyGui,
optionalStop: Boolean(action.enabled),
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: "optional stop set from Web control",
},
operatorMessage: `optional stop ${action.enabled ? "on" : "off"}`,
});
break;
case "SET_IGNORE_LIMITS":
setState({
gmoccapyGui: {
...state.gmoccapyGui,
ignoreLimits: Boolean(action.enabled),
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: "ignore limits set from Web checkbox",
},
operatorMessage: action.enabled ? "limit override requested" : "limit override clear",
});
break;
case "TOGGLE_COOLANT":
{
const gate = gateLinuxCncTaskAction(state, action);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
setState({
coolant: {
...state.coolant,
[action.kind]: !state.coolant[action.kind],
},
operatorMessage: `${action.kind} coolant toggled`,
});
}
break;
case "RELOAD_PROGRAM":
setState({
machine: {
...state.machine,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: "idle",
activeLine: state.programStartLine === 496 ? 501 : state.programStartLine,
programExecutionMotionIndex: 0,
programExecutionSampleIndex: 0,
programRuntimeFeedback: null,
programLineExecution: {},
axisPose: initialAxisPose,
preview: { ...state.preview, pathPoints: Math.max(state.programLines.length, 1) },
operatorMessage: "program reloaded",
});
break;
default:
throw new Error(`Unknown action type: ${action.type}`);
}
};
const refreshAsyncKinematicsFrame = async ({ operatorMessage = state.operatorMessage } = {}) => {
if (state.taskHalPauseLock?.active === true || isTaskHalRunPausedByOperator(state)) {
return state.rtcpFrame;
}
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") {
return state.rtcpFrame;
}
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
return state.rtcpFrame;
}
const sequence = state.asyncFrameRefreshSequence + 1;
state = {
...state,
asyncFrameRefreshPending: true,
asyncFrameRefreshSequence: sequence,
};
notify();
try {
await switchKinematicsRuntimeForState(state);
const frameSource = await state.kinematicsRuntime.frameForJoints(
jointsFromAxisPose(state.axisPose, state.profile),
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
);
if (state.asyncFrameRefreshSequence !== sequence) {
return state.rtcpFrame;
}
if (state.taskHalPauseLock?.active === true || isTaskHalRunPausedByOperator(state)) {
state = {
...state,
asyncFrameRefreshPending: false,
};
notify();
return state.rtcpFrame;
}
const frame = buildRtcpFrame({
axisPose: state.axisPose,
activeLine: state.activeLine,
kinsType: state.kinsType,
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
sourceMode: "source-derived-kinematics-wasm",
profile: state.profile,
linuxCncKinematicsResult: frameSource,
});
const nextState = {
...state,
sourceMode: "source-derived-kinematics-wasm",
frameSourceMode: "source-derived-kinematics-wasm",
desiredFrameSourceMode: "source-derived-kinematics-wasm",
axisPose: frame.axisPose,
jointPose: frame.jointPose,
tcpPose: frame.tcpPose,
toolAxisVector: frame.toolAxisVector,
rtcpState: frame.rtcpState,
rtcpFrame: frame,
lastKinematicsResult: frameSource,
dro: buildDroFromFrame(frame, state.programRuntimeFeedback),
asyncFrameRefreshPending: false,
operatorMessage,
};
const taskPolicy = createLinuxCncTaskPolicyStatus(nextState);
const derivedState = {
...nextState,
linuxCncTaskPolicy: taskPolicy,
machineProject: createMachineProjectState(nextState),
programValidation: createProgramValidationState(nextState),
rightSidebarEntrances: createRightSidebarEntranceState(nextState, taskPolicy),
};
state = {
...derivedState,
linuxCncParityMatrix: createLinuxCncParityMatrix(derivedState),
linuxCncProcessMonitor: createLinuxCncProcessMonitor(derivedState),
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(derivedState),
};
notify();
return frame;
} catch (error) {
if (state.asyncFrameRefreshSequence !== sequence) {
return state.rtcpFrame;
}
state = {
...state,
asyncFrameRefreshPending: false,
operatorMessage: `LinuxCNC kinematics refresh failed: ${error instanceof Error ? error.message : String(error)}`,
};
notify();
return state.rtcpFrame;
}
};
const saveSession = async (options = {}) => {
dispatch({ type: "SESSION_SAVE_STARTED" });
try {
const storageOptions = withDefaultWebOpfsRequirement(options);
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
const filename = options.filename || state.sessionPersistence.filename;
const payload = createFiveAxisSessionPayload(state);
const { snapshot, path, storageMode, storageCapability } = await saveFiveAxisSessionSnapshot(sessionId, payload, {
filename,
storage: storageOptions.storage,
storageMode: storageOptions.storageMode,
requireOpfs: storageOptions.requireOpfs,
metadata: options.metadata,
});
dispatch({
type: "SESSION_SAVE_COMPLETE",
path,
storageMode,
storageCapability,
savedAt: snapshot.createdAt,
});
return { snapshot, path, storageMode, storageCapability };
} catch (error) {
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
throw error;
}
};
const restoreSession = async (options = {}) => {
dispatch({ type: "SESSION_RESTORE_STARTED" });
try {
const storageOptions = withDefaultWebOpfsRequirement(options);
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
const filename = options.filename || state.sessionPersistence.filename;
const { snapshot, path, storageMode, storageCapability } = await loadFiveAxisSessionSnapshot(sessionId, {
filename,
storage: storageOptions.storage,
storageMode: storageOptions.storageMode,
requireOpfs: storageOptions.requireOpfs,
});
dispatch({
type: "SESSION_RESTORE_COMPLETE",
restoredState: restoreFiveAxisSessionState(snapshot),
path,
storageMode,
storageCapability,
restoredAt: new Date().toISOString(),
});
await refreshAsyncKinematicsFrame({ operatorMessage: `5-axis session restored ${path}` });
return { snapshot, path, storageMode, storageCapability };
} catch (error) {
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
throw error;
}
};
const stageMachineFiles = async (options = {}) => {
dispatch({ type: "MACHINE_FILE_STAGING_STARTED" });
try {
const storageOptions = withDefaultWebOpfsRequirement(options);
const { plan, save } = await stageProfileMachineFiles(state.profile, {
...options,
...storageOptions,
iniText: options.iniText || state.linuxCncIniConfig?.sourceText,
});
dispatch({ type: "MACHINE_FILE_STAGING_COMPLETE", plan, save });
return { plan, save };
} catch (error) {
dispatch({
type: "MACHINE_FILE_STAGING_FAILED",
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
};
const queryToolDb = (selector = {}) => {
if (!state.toolDbSimulation) return null;
if (selector.all === true || selector.list === true) {
return listToolEntries(state.toolDbSimulation);
}
return queryToolEntry(state.toolDbSimulation, selector);
};
const editToolDb = (patch = {}) => {
if (!state.toolDbSimulation) {
throw new Error("tool DB simulation is not ready");
}
const toolDbSimulation = editToolEntry(state.toolDbSimulation, patch);
setState({
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
}),
operatorMessage: `tool DB edited T${patch.toolNumber ?? patch.toolno ?? "-"}`,
});
return state.toolDbSimulation;
};
const saveToolDb = async (options = {}) => {
if (!state.toolDbSimulation) {
throw new Error("tool DB simulation is not ready");
}
const save = await saveToolDbSimulation(state.toolDbSimulation, withDefaultWebOpfsRequirement(options));
setState({
toolDbSimulation: save.toolDb,
toolDbReadiness: createToolDbReadiness(save.toolDb),
toolRuntimeState: createToolRuntimeState(save.toolDb, {
fallbackToolLength: state.toolPreview?.length,
}),
operatorMessage: `tool DB saved ${save.path} (${save.storageMode})`,
});
return save;
};
const runFullBoundaryAudit = async (options = {}) => {
if (state.machineFileStaging?.status !== "staged" || options.restage === true) {
await stageMachineFiles(options);
}
if (!state.machineFileStaging.selectedGcodeSourceRel) {
const sourceRel = options.sourceRel || defaultLinuxCncGcodeSourceForState(state)?.sourceRel;
if (sourceRel) {
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
await Promise.resolve();
}
}
dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
};
const initializeTaskHalSession = async ({ openProgram = true } = {}) => {
if (!state.taskHalRuntime?.loaded || !state.machineFileStaging?.save?.files?.length) {
return null;
}
stopTaskHalStatusLoop("session-initialize", { notify: false });
const preserveMachine = {
powerOn: state.machine.powerOn,
estopActive: state.machine.estopActive,
taskState: state.machine.taskState,
mode: state.machine.mode,
allHomed: state.machine.allHomed,
};
const selectedPlan = selectMachineFileProgramForState(state);
const session = buildTaskHalSessionFromMachineFiles({
profile: state.profile,
plan: selectedPlan,
save: state.machineFileStaging.save,
selectedProgramRel: state.machineFileStaging.selectedGcodeSourceRel,
});
if (session.programSourceRel && session.programSourceRel !== state.machineFileStaging.selectedGcodeSourceRel) {
return null;
}
await state.taskHalRuntime.resetSession?.();
await state.taskHalRuntime.initSession({
profileId: session.profileId,
iniPath: session.iniPath,
iniText: session.iniText,
programPath: session.programPath,
semanticBoundary: session.semanticBoundary,
});
await state.taskHalRuntime.stageFiles(session.files);
if (openProgram && session.programPath) {
await state.taskHalRuntime.openProgram(session.programPath);
await loadTaskHalMotionPlanForSession(session);
}
if (preserveMachine.powerOn) {
await state.taskHalRuntime.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" });
}
if (preserveMachine.allHomed) {
await state.taskHalRuntime.sendCommand({ type: "EMC_JOINT_HOME", joint: -1 });
}
await state.taskHalRuntime.sendCommand({
type: "EMC_TASK_SET_MODE",
mode: normalizeLinuxCncTaskMode(preserveMachine.mode).toUpperCase(),
});
await state.taskHalRuntime.runCycles({
...deriveTaskHalCyclePeriods(state),
taskCycles: 1,
});
if (session.programSourceRel && session.programSourceRel !== state.machineFileStaging.selectedGcodeSourceRel) {
return null;
}
dispatch({ type: "TASK_HAL_SESSION_READY", session });
const status = await state.taskHalRuntime.readStatus();
const statusPreserveMachine = isTaskHalRunPausedByOperator(state)
? { ...state.machine }
: preserveMachine;
dispatch({
type: "TASK_HAL_STATUS_APPLIED",
status,
preserveMachine: statusPreserveMachine,
operatorMessage: `LinuxCNC task/HAL session ready ${session.programPath || "-"}`,
});
return session;
};
const runValidatedTaskHalProgramRun = async () => {
const preflight = validateRunPreconditions(state, { requireTaskHalSession: false });
if (!preflight.ok) {
setState({ operatorMessage: preflight.operatorMessage });
return null;
}
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
await initializeTaskHalSession({ openProgram: true });
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
}
const loadedMotionPlan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
if (!loadedMotionPlan) {
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
return null;
}
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
if (!ready.ok) {
setState({ operatorMessage: ready.operatorMessage });
return null;
}
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
stopTaskHalStatusLoop("restarted", {
operatorMessage: "task/HAL status loop restarting",
});
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
const status = await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
], {
taskCycles: 5,
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
if (isTaskHalRunPausedByOperator(state)) {
return status;
}
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
profileId: ready.profileId,
iniPath: ready.iniPath,
kinematicsModuleId: ready.kinematicsModuleId,
operatorMessage: `task/HAL status loop running ${ready.profileId} ${ready.kinematicsModuleId}`,
});
}
return status;
};
const runReadySequence = async () => {
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
const sourceRel = defaultLinuxCncGcodeSourceForState(state)?.sourceRel;
if (sourceRel) {
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
}
}
if (state.taskHalRuntime?.loaded) {
await initializeTaskHalSession({ openProgram: true });
setState({
operatorMessage: state.machine.powerOn && state.machine.allHomed
? "RUN ready: program opened; press Run"
: "RUN setup ready: reset ESTOP, power on, Home All, then Run",
});
return state;
}
setState({
operatorMessage: state.machine.powerOn && state.machine.allHomed
? "RUN ready: program opened; press Run"
: "RUN setup ready: reset ESTOP, power on, Home All, then Run",
});
return state;
};
const operatorRunSequence = async () => {
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
const sourceRel = defaultLinuxCncGcodeSourceForState(state)?.sourceRel;
if (sourceRel) {
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
}
}
const runGateState = {
...state,
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
},
};
const runGate = gateLinuxCncTaskAction(runGateState, { type: "RUN" });
if (!runGate.allowed) {
setState({ operatorMessage: runGate.operatorMessage });
return state;
}
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
if (state.taskHalRuntime?.loaded) {
if (
!Array.isArray(state.programExecution?.motion)
|| state.programExecution.motion.length === 0
|| state.interpreterExecutionPending
) {
if (!state.interpreterExecutionPending) {
dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
}
await waitForStatePolling((nextState) => (
nextState.interpreterExecutionPending === false
&& Array.isArray(nextState.programExecution?.motion)
&& nextState.programExecution.motion.length > 0
), 15000);
}
if (tcpKinsType && state.kinsType !== tcpKinsType) {
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
}
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
taskHalPauseLock: null,
operatorMessage: "RUN ready: motion plan source ready; preparing task/HAL session",
});
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
setState({ operatorMessage: "RUN preparing: initializing task/HAL session" });
await initializeTaskHalSession({ openProgram: true });
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
}
setState({ operatorMessage: "RUN preparing: loading task/HAL motion plan" });
const loadedMotionPlan = await loadTaskHalMotionPlanWithSessionRetry();
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
if (!loadedMotionPlan) {
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
return state;
}
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
if (!ready.ok) {
setState({ operatorMessage: ready.operatorMessage });
return state;
}
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
stopTaskHalStatusLoop("restarted", {
operatorMessage: "task/HAL status loop restarting",
});
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
setState({
activeLine: state.programStartLine || 1,
programExecutionMotionIndex: 0,
programExecutionSampleIndex: 0,
programRuntimeFeedback: null,
programLineExecution: {},
operatorMessage: "RUN executing: sending task/HAL PLAN_RUN",
});
const status = await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
], {
taskCycles: 5,
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
profileId: ready.profileId,
iniPath: ready.iniPath,
kinematicsModuleId: ready.kinematicsModuleId,
operatorMessage: `task/HAL status loop running ${ready.profileId} ${ready.kinematicsModuleId}`,
});
}
return state;
}
const policy = createLinuxCncTaskPolicyStatus(state);
if (policy.taskMode !== "auto") {
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
},
operatorMessage: "mode auto",
});
}
dispatch({ type: "RUN" });
return state;
};
const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => {
if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") {
return null;
}
setState({ operatorMessage: "RUN preparing: building task/HAL motion plan" });
const motion = state.programExecution?.motion || [];
const timing = state.programExecutionTiming || buildTimingForState(state, state.programExecution);
if (!session?.programPath || !Array.isArray(motion) || motion.length === 0 || !Array.isArray(timing?.segments) || timing.segments.length === 0) {
return null;
}
const previewPlan = buildTaskHalProgramMotionPlanFromPreviewPath({
programPath: session.programPath,
path: state.programAxisPreviewPath,
profile: state.profile,
linearUnits: timing.linearUnits || state.profile?.traj?.linearUnits || "mm",
});
const plan = previewPlan || buildTaskHalProgramMotionPlan({
programPath: session.programPath,
motion,
timing,
programLines: state.programLines,
linearUnits: timing.linearUnits || state.profile?.traj?.linearUnits || "mm",
});
if (plan.segmentCount <= 0) {
return null;
}
setState({ operatorMessage: `RUN preparing: worker loading ${plan.segmentCount} task/HAL motion segments` });
await withTimeout(
state.taskHalRuntime.loadProgramMotionPlan(plan),
5000,
"task/HAL feed motion plan load timed out",
);
return plan;
};
const loadTaskHalMotionPlanWithSessionRetry = async () => {
let plan = null;
try {
plan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
} catch (error) {
setState({ operatorMessage: `task/HAL motion plan reload: ${error instanceof Error ? error.message : String(error)}` });
}
if (plan) return plan;
await initializeTaskHalSession({ openProgram: true });
return loadTaskHalMotionPlanForSession(state.taskHalSession);
};
const startTaskHalStatusLoop = ({
profileId = state.machineProfile,
iniPath = state.profile?.iniPath || null,
kinematicsModuleId = state.profile?.kinematicsModuleId || state.machineProfile,
batchSize = 5,
intervalMs = 25,
taskPeriodNs = deriveTaskHalCyclePeriods(state).taskPeriodNs,
servoPeriodNs = deriveTaskHalCyclePeriods(state).servoPeriodNs,
operatorMessage = "task/HAL status loop running",
} = {}) => {
if (!state.taskHalRuntime?.loaded) return null;
stopTaskHalStatusLoop("restarted", { notify: false });
const sequence = Number(state.taskHalStatusLoop?.sequence || 0) + 1;
dispatch({
type: "TASK_HAL_STATUS_LOOP_STARTED",
sequence,
profileId,
iniPath,
kinematicsModuleId,
batchSize,
intervalMs,
taskPeriodNs,
servoPeriodNs,
operatorMessage,
});
const tick = () => runTaskHalStatusLoopTick(sequence).catch((error) => {
stopTaskHalStatusLoop("error", {
error: error instanceof Error ? error.message : String(error),
operatorMessage: `task/HAL status loop failed: ${error instanceof Error ? error.message : String(error)}`,
});
});
taskHalStatusLoopTimer = setTimeout(tick, intervalMs);
return sequence;
};
const runTaskHalStatusLoopTick = async (sequence) => {
const loop = state.taskHalStatusLoop || {};
if (!loop.active || loop.sequence !== sequence || !state.taskHalRuntime?.loaded) {
return null;
}
await state.taskHalRuntime.runCycles({
taskPeriodNs: loop.taskPeriodNs,
servoPeriodNs: loop.servoPeriodNs,
taskCycles: loop.batchSize,
});
const status = await state.taskHalRuntime.readStatus();
if (state.taskHalStatusLoop?.sequence !== sequence || state.taskHalStatusLoop?.active !== true) {
return status;
}
dispatch({
type: "TASK_HAL_STATUS_APPLIED",
status,
loopSequence: sequence,
operatorMessage: `task/HAL status tick ${Number(state.taskHalStatusLoop?.tickCount || 0) + 1}`,
});
if (shouldContinueTaskHalStatusLoop(state, status)) {
taskHalStatusLoopTimer = setTimeout(
() => runTaskHalStatusLoopTick(sequence).catch((error) => {
stopTaskHalStatusLoop("error", {
error: error instanceof Error ? error.message : String(error),
operatorMessage: `task/HAL status loop failed: ${error instanceof Error ? error.message : String(error)}`,
});
}),
Number(state.taskHalStatusLoop?.intervalMs || loop.intervalMs || 25),
);
} else {
stopTaskHalStatusLoop(state.runState === "complete" ? "complete" : state.runState, {
operatorMessage: state.runState === "complete"
? "task/HAL program complete"
: `task/HAL status loop ${state.runState}`,
});
}
return status;
};
const stopTaskHalStatusLoop = (reason = "stopped", {
error = null,
operatorMessage = null,
notify = true,
} = {}) => {
if (taskHalStatusLoopTimer) {
clearTimeout(taskHalStatusLoopTimer);
taskHalStatusLoopTimer = null;
}
if (notify && (state.taskHalStatusLoop?.active || state.taskHalStatusLoop?.stopReason !== reason || error)) {
dispatch({
type: "TASK_HAL_STATUS_LOOP_STOPPED",
reason,
error,
operatorMessage,
});
}
};
const runTaskHalCommandSequence = async (commands, {
taskCycles = 1,
taskPeriodNs = deriveTaskHalCyclePeriods(state).taskPeriodNs,
servoPeriodNs = deriveTaskHalCyclePeriods(state).servoPeriodNs,
operatorMessage = "task/HAL command complete",
pendingJogCommand = null,
allowFixtureSession = true,
preserveMachine = null,
preserveAxisPose = null,
} = {}) => {
if (!state.taskHalRuntime?.loaded) {
throw new Error("LinuxCNC task/HAL runtime not attached");
}
const sequence = state.taskHalExecutionSequence + 1;
setState({
taskHalExecutionPending: true,
taskHalExecutionSequence: sequence,
pendingJogCommand,
operatorMessage: "LinuxCNC task/HAL command running",
});
try {
if (!state.taskHalSession && state.machineFileStaging?.save?.files?.length) {
await initializeTaskHalSession({ openProgram: true });
} else if (allowFixtureSession && !state.taskHalSession && state.programLines?.length) {
await initializeFixtureTaskHalSessionForState();
}
if (!state.taskHalSession) {
throw new Error("LinuxCNC task/HAL session not initialized");
}
for (const command of commands) {
await state.taskHalRuntime.sendCommand(command);
}
await state.taskHalRuntime.runCycles({ taskPeriodNs, servoPeriodNs, taskCycles });
const status = await state.taskHalRuntime.readStatus();
if (state.taskHalExecutionSequence !== sequence) {
return status;
}
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage, preserveMachine, preserveAxisPose });
return status;
} catch (error) {
dispatch({
type: "TASK_HAL_COMMAND_FAILED",
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
};
const initializeFixtureTaskHalSessionForState = async () => {
if (!state.taskHalRuntime?.loaded) return null;
const programPath = "web-ui/current-program.ngc";
const iniText = state.linuxCncIniConfig?.sourceText || `[TRAJ]\nCOORDINATES = ${state.profile.traj.coordinates.split("").join(" ")}\n`;
await state.taskHalRuntime.resetSession?.();
await state.taskHalRuntime.initSession({
profileId: state.machineProfile,
iniPath: state.profile.iniPath,
iniText,
programPath,
semanticBoundary: "linuxcnc_task_hal_fixture_program_session",
});
await state.taskHalRuntime.stageFiles([
{
sourceRel: state.programSourceRel || state.activeProgram,
wasmPath: programPath,
path: programPath,
kind: "demo",
text: state.programLines.join("\n"),
bytes: state.programLines.join("\n").length,
},
]);
await state.taskHalRuntime.openProgram(programPath);
const session = {
apiName: "web-rtcp-5axis-task-hal-fixture-session",
semanticBoundary: "linuxcnc_task_hal_fixture_program_session",
profileId: state.machineProfile,
iniPath: state.profile.iniPath,
programPath,
fileCount: 1,
};
dispatch({ type: "TASK_HAL_SESSION_READY", session });
return session;
};
const scheduleAsyncKinematicsRefresh = () => {
if (state.taskHalPauseLock?.active === true || isTaskHalRunPausedByOperator(state)) return null;
if (state.asyncFrameRefreshPending) return null;
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") return null;
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
const frame = state.rtcpFrame;
if (
frame?.sourceMode === "source-derived-kinematics-wasm" &&
frame.readiness?.linuxCncKinematicsReady === true &&
frame.activeLine === state.activeLine &&
frame.kinsType === state.kinsType &&
frame.axisPose?.x === state.axisPose.x &&
frame.axisPose?.y === state.axisPose.y &&
frame.axisPose?.z === state.axisPose.z &&
frame.axisPose?.a === state.axisPose.a &&
frame.axisPose?.b === state.axisPose.b &&
frame.axisPose?.c === state.axisPose.c
) {
return null;
}
return refreshAsyncKinematicsFrame();
};
return {
getState: () => state,
subscribe(listener) {
listeners.add(listener);
listener(state);
return () => listeners.delete(listener);
},
dispatch,
refreshKinematicsFrame: refreshAsyncKinematicsFrame,
saveSession,
restoreSession,
stageMachineFiles,
queryToolDb,
editToolDb,
saveToolDb,
runFullBoundaryAudit,
initializeTaskHalSession,
};
}
function buildFrameForState(state, patch = {}) {
const requestedSourceMode = state.desiredFrameSourceMode || state.frameSourceMode || state.sourceMode;
let linuxCncKinematicsResult = patch.lastKinematicsResult || null;
let sourceMode = requestedSourceMode;
if (requestedSourceMode === "source-derived-kinematics-wasm") {
if (state.kinematicsRuntime?.loaded && !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
switchKinematicsRuntimeForState(state);
linuxCncKinematicsResult = state.kinematicsRuntime.frameForJoints(
jointsFromAxisPose(state.axisPose, state.profile),
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
);
} else if (state.kinematicsRuntime?.loaded && isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
sourceMode = "fixture-ui-only";
linuxCncKinematicsResult = null;
} else {
sourceMode = "fixture-ui-only";
linuxCncKinematicsResult = null;
}
}
const frame = buildRtcpFrame({
axisPose: state.axisPose,
activeLine: state.activeLine,
kinsType: state.kinsType,
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
sourceMode,
profile: state.profile,
linuxCncKinematicsResult,
});
return {
frame,
lastKinematicsResult: linuxCncKinematicsResult,
};
}
function createKinematicsDescriptor(runtime) {
if (!runtime?.loaded) return null;
return {
apiName: runtime.apiName,
moduleId: runtime.moduleId,
wasmFile: runtime.wasmFile,
supportedModules: runtime.supportedModules,
loaded: runtime.loaded,
sourceMode: runtime.sourceMode,
semanticBoundary: runtime.semanticBoundary,
executionContext: runtime.executionContext || "direct",
workerUrl: runtime.workerUrl || null,
switchkinsType: runtime.switchkinsType,
switchRc: runtime.switchRc,
};
}
function createInterpreterDescriptor(runtime) {
if (!runtime?.loaded) return null;
return {
apiName: runtime.apiName,
loaded: runtime.loaded,
sourceMode: runtime.sourceMode,
semanticBoundary: runtime.semanticBoundary,
executionContext: runtime.executionContext || "direct",
};
}
function selectMachineFileProgramForState(state) {
const sourceRel = state.machineFileStaging?.selectedGcodeSourceRel;
if (!sourceRel) return state.machineFileStaging.plan;
return selectMachineFileProgram(
state.machineFileStaging.plan,
state.machineFileStaging.save,
sourceRel,
);
}
function defaultLinuxCncGcodeSourceForState(state) {
const sources = state.machineFileStaging?.gcodeSources || [];
const defaultProgramFilename = state.profile?.machineFileStaging?.defaultProgramFilename;
if (defaultProgramFilename) {
const match = sources.find((source) => source.filename === defaultProgramFilename);
if (match) return match;
}
const preferred = `${state.machineProfile || "xyzac-trt"}_switchkins.ngc`;
return sources.find((source) => source.filename === preferred)
|| sources.find((source) => source.filename.includes(state.machineProfile || "xyzac"))
|| sources[0]
|| null;
}
function createInitialControlledUserMState() {
const simulation = createControlledUserMSimulation();
return {
controlledUserMSimulation: simulation,
controlledUserMReadiness: createControlledUserMReadiness(simulation),
};
}
function createToolDbStatePatchFromStagedFiles({ profile, save }) {
const toolTableFile = findStagedToolTableFile(profile, save);
if (!toolTableFile) {
return {
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: 84.019,
}),
};
}
const toolTable = parseLinuxCncToolTable(toolTableFile.text, {
sourceRel: toolTableFile.sourceRel,
path: toolTableFile.wasmPath || toolTableFile.path || null,
});
const toolDbSimulation = createToolDbSimulation({
toolTable,
profile,
storageMode: save?.storageMode || null,
});
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: 84.019,
}),
};
}
function createProgramToolUserSimulationPatch({ state, programText, sourceRel }) {
const toolCommands = extractToolCommandSequenceFromProgram(programText);
const toolDbSimulation = state.toolDbSimulation && toolCommands.length > 0
? applyToolCommandSequence(state.toolDbSimulation, toolCommands)
: state.toolDbSimulation;
const userMScan = runControlledUserMProgramScan(
state.controlledUserMSimulation || createControlledUserMSimulation(),
programText,
{
profile: state.profile,
sourceRel,
},
);
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
}),
controlledUserMSimulation: userMScan.simulation,
controlledUserMReadiness: createControlledUserMReadiness(userMScan.simulation),
};
}
function findStagedToolTableFile(profile, save) {
const files = Array.isArray(save?.files) ? save.files : [];
return files.find((file) => file.sourceRel === profile?.toolTablePath)
|| files.find((file) => file.kind === "toolTable" && file.sourceRel?.endsWith(`${profile?.id}.tbl`))
|| files.find((file) => file.kind === "toolTable")
|| null;
}
function createMachineProjectState(state = {}) {
const profile = state.profile || {};
const profileId = profile.id || state.machineProfile || "unknown";
const staging = state.machineFileStaging || {};
const save = staging.save || {};
const files = Array.isArray(save.files) ? save.files : [];
const gcodeFiles = Array.isArray(staging.gcodeFiles) && staging.gcodeFiles.length > 0
? staging.gcodeFiles
: Array.isArray(save.gcodeFiles)
? save.gcodeFiles
: listProjectGcodeFiles(save);
const configFiles = files
.filter((file) => !["demo", "remap"].includes(file.kind))
.map(projectFileDescriptor);
const iniFile = files.find((file) => file.kind === "ini" && file.sourceRel === profile.iniPath)
|| files.find((file) => file.kind === "ini")
|| null;
const selectedProgram = gcodeFiles.find((file) => file.sourceRel === staging.selectedGcodeSourceRel)
|| null;
const projectRoot = staging.opfsRoot || `${MACHINE_PROJECT_OPFS_ROOT}/${profileId}`;
const machineRel = staging.plan?.machineRel || profile.machineFileStaging?.machineRel || "axis/vismach/5axis/table-rotary-tilting";
const demoDirectory = staging.plan?.demoDirectory || profile.machineFileStaging?.demoDirectory || "demos";
return {
apiName: "web-rtcp-5axis-machine-project",
profileId,
projectRoot,
status: staging.status || "not-staged",
storageMode: staging.storageMode || save.storageMode || null,
storageCapability: staging.storageCapability || save.storageCapability || null,
linuxCncIniSourceRel: profile.iniPath || null,
linuxCncIniLoaded: state.iniConfigReadiness?.loaded === true,
ini: iniFile
? {
...projectFileDescriptor(iniFile),
sourceMatchesProfile: iniFile.sourceRel === profile.iniPath,
textMatchesLoadedIni: typeof state.linuxCncIniConfig?.sourceText === "string"
? iniFile.text === state.linuxCncIniConfig.sourceText
: null,
contentBoundary: "linuxcnc_ini_source_text_staged_without_web_rewrite",
}
: null,
configFiles,
configFileCount: configFiles.length,
gcodeDirectory: `${projectRoot}/configs/sim/${machineRel}/${demoDirectory}`,
gcodeFiles: gcodeFiles.map(projectFileDescriptor),
gcodeFileCount: gcodeFiles.length,
selectedProgram: selectedProgram ? projectFileDescriptor(selectedProgram) : null,
fileKindCounts: countProjectFileKinds(files),
semanticBoundary: "linuxcnc_ini_project_directory_machine_config_and_gcode_files",
};
}
function createProgramValidationState(state = {}) {
const execution = state.programExecution || null;
const summary = execution?.summary || {};
const motion = Array.isArray(execution?.motion) ? execution.motion : [];
const selectedSourceRel = state.machineFileStaging?.selectedGcodeSourceRel || state.programSourceRel || null;
const linuxCncFiveAxisSource = isLinuxCncFiveAxisDemoSource(selectedSourceRel);
const motionEventCount = Number(summary.motionEventCount ?? motion.length ?? 0);
const canonicalEventCount = Number(summary.canonicalEventCount ?? 0);
const switchkinsEventCount = Number(summary.switchkinsEventCount ?? 0);
const timing = state.programExecutionTiming || execution?.plannerTiming || null;
const plannerSamples = Array.isArray(execution?.plannerTiming?.samples)
? execution.plannerTiming.samples
: [];
const sampleCount = Array.isArray(timing?.samples) && timing.samples.length > 0
? timing.samples.length
: plannerSamples.length;
const ready = Boolean(summary.ready && motionEventCount > 0);
return {
apiName: "web-rtcp-5axis-program-validation",
status: state.interpreterExecutionPending
? "validating"
: ready
? "validated"
: "pending",
ready,
activeProgram: state.activeProgram || null,
programSource: state.programSource || null,
programSourceRel: selectedSourceRel,
sourceGuard: linuxCncFiveAxisSource
? "linuxcnc_vendored_5axis_gcode_source_file"
: state.programSource === "operator-file"
? "operator_file_not_promoted_to_linuxcnc_source"
: "fixture_or_mdi_program_not_linuxcnc_source",
previewSource: execution?.sourceMode === "linuxcnc-interpreter-wasm"
? "linuxcnc_interpreter_canonical_motion"
: execution?.sourceMode || state.programExecutionSourceMode || "fixture-line-playback",
executionTraceSource: state.programRuntimeFeedback?.sourceMode
|| (plannerSamples.length > 0 ? "linuxcnc_tp_samples" : state.programExecutionSourceMode),
motionEventCount,
canonicalEventCount,
switchkinsEventCount,
plannerSampleCount: sampleCount,
plannerRuntimeReady: execution?.plannerTiming?.plannerRuntimeReady === true || timing?.plannerRuntimeReady === true,
remapRuntimeReady: summary.remapRuntimeReady === true,
currentLine: Number(state.programRuntimeFeedback?.line || state.activeLine || 0),
currentMotionIndex: Number(state.programExecutionMotionIndex || 0),
currentSampleIndex: Number(state.programExecutionSampleIndex || 0),
axisFeedbackSource: state.programRuntimeFeedback?.sourceMode || state.programExecutionSourceMode || "ui-state",
realtimeAxisValues: {
...pickExecutionAxes(state.axisPose || {}),
tcpX: Number(state.tcpPose?.x || 0),
tcpY: Number(state.tcpPose?.y || 0),
tcpZ: Number(state.tcpPose?.z || 0),
},
rtcpState: state.rtcpState || "off",
kinsType: state.kinsType || "identity",
semanticBoundary: "program_validation_consumes_linuxcnc_interpreter_tp_and_task_hal_feedback",
};
}
function createLinuxCncProcessMonitor(state = {}) {
const feedback = state.programRuntimeFeedback || {};
const uiExecution = state.programUiExecution || createProgramUiExecution(state);
const status = state.taskHalStatus || {};
const ui = status.ui || {};
const motion = status.motionStatus?.motion || {};
const halPins = status.halSnapshot?.pins || {};
const toolRuntime = state.toolRuntimeState || {};
const toolDb = state.toolDbSimulation || {};
const currentTool = toolRuntime.currentTool || toolRuntime.activeToolOffset || toolRuntime.pathTool || null;
const path = state.programAxisPreviewPath || null;
const previewTool = path?.samples?.find((sample) => sample?.tool)?.tool || null;
const runtimePathTool = toolRuntime.pathTool || null;
const pathTool = Number(runtimePathTool?.id || 0) > 0 || Number(runtimePathTool?.diameter || 0) > 0
? runtimePathTool
: previewTool;
const sampleCount = Number(path?.sampleCount || path?.samples?.length || 0);
const sampleIndex = clampNumber(
Number(state.programExecutionSampleIndex ?? feedback.sampleIndex ?? 0),
0,
Math.max(sampleCount - 1, 0),
);
const motionCount = Number(state.programExecution?.summary?.motionEventCount || state.programExecution?.motion?.length || 0);
const motionIndex = clampNumber(
Number(state.programExecutionMotionIndex ?? feedback.motionIndex ?? 0),
0,
Math.max(motionCount - 1, 0),
);
const sourceLine = Number(feedback.line || state.activeLine || 0);
const activeGcode = uiExecution?.statement || (sourceLine > 0
? state.programLines?.[sourceLine - Number(state.programStartLine || 1)] || ""
: "");
const currentVelocity = Number(feedback.currentVelocityMmPerMin ?? state.feed?.currentVelocity ?? ui.currentVelocity ?? 0);
const requestedVelocity = Number(feedback.requestedVelocityMmPerMin ?? motion.requestedVel * 60 ?? currentVelocity);
const feedRate = Number(state.feed?.feedRate || currentVelocity || 0);
const spindleCommandRpm = Number(state.spindle?.rpm || 0);
const spindleActualRpm = state.spindle?.enabled
? spindleCommandRpm * (Number(state.spindle?.override || 100) / 100)
: 0;
const spindleHalPins = state.spindle?.halPins || {};
const toolChange = {
toolInSpindle: Number(toolRuntime.toolInSpindle || toolDb.toolInSpindle || 0),
toolFromPocket: Number(toolRuntime.toolFromPocket || toolDb.toolFromPocket || 0),
currentPocket: Number(toolRuntime.currentPocket || toolDb.currentPocket || 0),
preparedTool: Number(toolDb.preparedTool || 0),
preparedPocket: Number(toolDb.preparedPocket || 0),
activeToolNumber: Number(toolRuntime.activeToolNumber || pathTool?.id || currentTool?.toolNumber || 0),
activePocket: Number(toolRuntime.activePocket || pathTool?.pocket || currentTool?.pocket || 0),
diameter: Number(pathTool?.diameter ?? currentTool?.diameter ?? 0),
lengthOffsetZ: Number(toolRuntime.kinematics?.toolOffsetZ ?? currentTool?.offset?.z ?? pathTool?.length ?? 0),
activeOffsetApplied: Boolean(toolRuntime.activeOffsetApplied),
iocontrol: {
toolPrepare: Boolean(toolDb.iocontrol?.toolPrepare),
toolPrepared: Boolean(toolDb.iocontrol?.toolPrepared),
toolChange: Boolean(toolDb.iocontrol?.toolChange),
toolChanged: Boolean(toolDb.iocontrol?.toolChanged),
toolPrepNumber: Number(toolDb.iocontrol?.toolPrepNumber || 0),
toolPrepPocket: Number(toolDb.iocontrol?.toolPrepPocket || 0),
},
emcioStatus: toolDb.emcioStatus?.status || "UNKNOWN",
};
return {
apiName: "web-rtcp-5axis-linuxcnc-process-monitor",
semanticBoundary: "linuxcnc_task_motion_hal_interpreter_tool_status_monitor",
sourceReferences: {
task: "linuxcnc/src/emc/task/emctaskmain.cc",
motion: "linuxcnc/src/emc/motion/control.c",
interpreter: "linuxcnc/src/emc/rs274ngc",
axisGui: "linuxcnc/src/emc/usr_intf/axis/scripts/axis.py",
halPins: "linuxcnc/src/emc/usr_intf/halui.cc + src/emc/iotask/ioControl.cc",
toolChange: "linuxcnc/src/emc/task/taskclass.cc + src/emc/tooldata/tooldata_common.cc",
},
control: {
powerOn: Boolean(state.machine?.powerOn),
estopActive: Boolean(state.machine?.estopActive),
taskState: state.machine?.taskState || "estop-reset",
taskMode: state.machine?.mode || "manual",
interpState: state.machine?.interpState || "idle",
runState: state.runState || "idle",
allHomed: Boolean(state.machine?.allHomed),
taskPaused: Boolean(state.machine?.taskPaused),
buttonParityCount: 57,
policy: state.linuxCncTaskPolicy || null,
},
path: {
activeProgram: state.activeProgram || null,
programSourceRel: state.programSourceRel || state.machineFileStaging?.selectedGcodeSourceRel || null,
previewSource: path?.source || state.programValidation?.previewSource || state.programExecutionSourceMode,
executionSource: feedback.sourceMode || state.programExecutionSourceMode,
activeLine: sourceLine,
sourceFile: uiExecution?.sourceFile || null,
sourceLine: Number(uiExecution?.line || sourceLine || 0),
activeGcode,
uiExecution,
motionIndex,
motionCount,
sampleIndex,
sampleCount,
elapsedSeconds: Number(state.programElapsedSeconds || feedback.timeSeconds || 0),
remainingSeconds: Number(state.programRemainingSeconds || 0),
queueDepth: Number(feedback.queueDepth || ui.motionQueueDepth || 0),
activeDepth: Number(feedback.activeDepth || 0),
lineExecution: state.programLineExecution?.[sourceLine] || null,
},
axes: {
linearUnits: state.profile?.traj?.linearUnits || feedback.linearUnits || "mm",
rotaryUnits: "deg",
positionSource: feedback.sourceMode || state.frameSourceMode || "ui-state",
dro: { ...(state.dro || {}) },
joint: { ...pickExecutionAxes(state.axisPose || {}) },
tcp: {
x: Number(state.tcpPose?.x || 0),
y: Number(state.tcpPose?.y || 0),
z: Number(state.tcpPose?.z || 0),
},
distanceToGo: {
x: Number(state.dro?.dtgX || 0),
y: Number(state.dro?.dtgY || 0),
z: Number(state.dro?.dtgZ || 0),
scalar: Number(feedback.distanceToGo || 0),
},
toolAxisVector: { ...(state.toolAxisVector || {}) },
kinsType: state.kinsType || "identity",
rtcpState: state.rtcpState || "off",
},
spindle: {
enabled: Boolean(state.spindle?.enabled),
direction: state.spindle?.direction || "stop",
commandRpm: spindleCommandRpm,
actualRpm: spindleActualRpm,
overridePercent: Number(state.spindle?.override || 0),
halPins: {
on: Number(spindleHalPins.on ?? halPins["spindle.0.on"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
forward: Number(spindleHalPins.forward ?? halPins["spindle.0.forward"]?.value ?? (state.spindle?.direction === "forward" ? 1 : 0)),
reverse: Number(spindleHalPins.reverse ?? halPins["spindle.0.reverse"]?.value ?? (state.spindle?.direction === "reverse" ? 1 : 0)),
speedOut: Number(spindleHalPins.speedOut ?? halPins["spindle.0.speed-out"]?.value ?? spindleActualRpm),
atSpeed: Number(spindleHalPins.atSpeed ?? halPins["spindle.0.at-speed"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
},
},
feed: {
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: requestedVelocity,
cuttingVelocityMmPerMin: isCuttingMotion(feedback) ? currentVelocity : 0,
feedRate,
feedOverridePercent: Number(state.feed?.feedOverride || 0),
rapidOverridePercent: Number(state.feed?.rapidOverride || 0),
feedMode: feedback.feedMode || "units-per-minute",
},
coolant: {
flood: Boolean(state.coolant?.flood),
mist: Boolean(state.coolant?.mist),
halPins: {
flood: Number(halPins["iocontrol.0.coolant-flood"]?.value ?? (state.coolant?.flood ? 1 : 0)),
mist: Number(halPins["iocontrol.0.coolant-mist"]?.value ?? (state.coolant?.mist ? 1 : 0)),
},
},
toolChange,
runtime: {
interpreterReady: state.interpreterRuntimeReadiness?.loaded === true,
kinematicsReady: state.kinematicsRuntimeReadiness?.loaded === true,
taskHalReady: state.taskHalRuntimeReadiness?.taskRuntimeReady === true
&& state.taskHalRuntimeReadiness?.motionRuntimeReady === true
&& state.taskHalRuntimeReadiness?.halRuntimeReady === true,
taskHalLoopActive: state.taskHalStatusLoop?.active === true,
taskHalTickCount: Number(state.taskHalStatusLoop?.tickCount || 0),
taskCycle: Number(feedback.taskCycle || ui.taskCycle || 0),
servoCycle: Number(feedback.cycle || ui.servoCycle || 0),
halChangedPinCount: Number(feedback.halChangedPinCount || ui.halChangedPinCount || 0),
fallbackReason: state.taskHalFallbackReason || null,
},
};
}
function isCuttingMotion(feedback = {}) {
const type = String(feedback.type || "").toUpperCase();
return type.includes("FEED") || type.includes("ARC") || type === "TASK_MOTION";
}
function createRightSidebarEntranceState(state = {}, taskPolicy = createLinuxCncTaskPolicyStatus(state)) {
const manualActive = state.machine?.mode === "manual" && state.machine?.manualPanel !== "jog";
const jogActive = state.machine?.mode === "manual" && state.machine?.manualPanel === "jog";
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
const powerGate = gateLinuxCncTaskAction(state, { type: "TOGGLE_POWER" });
const autoGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "auto" });
const manualGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "manual" });
const jogGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "jog" });
const mdiGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "mdi" });
const identityGate = gateKinsTypeChange(state, "identity");
const tcpGate = tcpKinsType
? gateKinsTypeChange(state, tcpKinsType)
: blockKinsChange(taskPolicy, `TCP blocked: ${state.profile?.id || "profile"} has no TCP switchkins type`);
return [
sidebarEntry({ id: "estop", label: "E-STOP", active: Boolean(state.machine?.estopActive), gate: allowKinsChange(taskPolicy), tone: "emergency" }),
sidebarEntry({ id: "power", label: "POWER", active: Boolean(state.machine?.powerOn), gate: powerGate, tone: "power" }),
sidebarEntry({ id: "reset", label: "RESET", active: false, gate: allowKinsChange(taskPolicy), tone: state.machine?.estopActive ? "reset-required" : "reset" }),
sidebarEntry({ id: "auto", label: "AUTO", active: state.machine?.mode === "auto", gate: autoGate, tone: "mode" }),
sidebarEntry({ id: "manual", label: "MANUAL", active: manualActive, gate: manualGate, tone: "mode" }),
sidebarEntry({ id: "jog", label: "JOG", active: jogActive, gate: jogGate, tone: "mode" }),
sidebarEntry({ id: "mdi", label: "MDI", active: state.machine?.mode === "mdi", gate: mdiGate, tone: "mode" }),
sidebarEntry({ id: "identity", label: "IDENTITY", active: state.kinsType === "identity", gate: identityGate, tone: "identity" }),
sidebarEntry({ id: "tcp", label: "TCP", active: String(state.kinsType || "").startsWith("tcp-"), gate: tcpGate, tone: "tcp" }),
];
}
function sidebarEntry({ id, label, active, gate, tone }) {
const allowed = gate?.allowed !== false;
const status = id === "estop" && active
? "emergency"
: active && !allowed
? "active-blocked"
: active
? "active"
: allowed
? "ready"
: "blocked";
return {
apiName: "web-rtcp-5axis-right-sidebar-entry",
id,
label,
active: Boolean(active),
allowed,
status,
tone,
operatorMessage: gate?.operatorMessage || null,
colorRule: `${tone}:${status}`,
semanticBoundary: "gmoccapy_right_vertical_button_task_state_mode_gate",
};
}
function gateKinsTypeChange(state = {}, requestedKinsType = "identity") {
const taskPolicy = createLinuxCncTaskPolicyStatus(state);
const target = String(requestedKinsType || "");
if (!target) {
return blockKinsChange(taskPolicy, "kinematics blocked: missing switchkins target");
}
if (target.startsWith("tcp-") && !profileSupportsTcp(state.profile)) {
return blockKinsChange(taskPolicy, `kinematics ${target} blocked: ${state.profile?.id || "profile"} is not TCP capable`);
}
if (taskPolicy.taskState === "estop") {
return blockKinsChange(taskPolicy, "kinematics blocked: reset estop first");
}
if (taskPolicy.taskState !== "on") {
return blockKinsChange(taskPolicy, "kinematics blocked: machine must be on");
}
if (taskPolicy.interpState === "reading" || taskPolicy.interpState === "waiting" || state.runState === "running") {
return blockKinsChange(taskPolicy, "kinematics blocked: interpreter must be idle");
}
return allowKinsChange(taskPolicy);
}
function allowKinsChange(status) {
return {
allowed: true,
status,
operatorMessage: null,
};
}
function blockKinsChange(status, operatorMessage) {
return {
allowed: false,
status,
operatorMessage,
};
}
function projectFileDescriptor(file = {}) {
return {
sourceRel: file.sourceRel || null,
filename: sourceBasename(file.sourceRel || file.path || file.wasmPath || ""),
opfsPath: file.opfsPath || null,
wasmPath: file.wasmPath || file.path || null,
kind: file.kind || "asset",
bytes: Number(file.bytes || (typeof file.text === "string" ? file.text.length : 0)),
semanticBoundary: file.semanticBoundary || "linuxcnc_project_file_staged_without_web_rewrite",
};
}
function countProjectFileKinds(files = []) {
return files.reduce((counts, file) => {
const kind = file.kind || "asset";
counts[kind] = (counts[kind] || 0) + 1;
return counts;
}, {});
}
function isLinuxCncFiveAxisDemoSource(sourceRel) {
const value = String(sourceRel || "");
return (
value.startsWith("configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/")
|| value.startsWith("configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/")
) && value.endsWith(".ngc");
}
function sourceBasename(path) {
return String(path || "").split("/").filter(Boolean).at(-1) || "";
}
function withTimeout(promise, timeoutMs, message) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
Promise.resolve(promise)
.then((value) => {
clearTimeout(timeoutId);
resolve(value);
})
.catch((error) => {
clearTimeout(timeoutId);
reject(error);
});
});
}
function isAsyncKinematicsRuntime(runtime) {
return runtime?.executionContext === "worker";
}
export function validateRunPreconditions(state = {}, {
requireTaskHalRuntime = true,
requireTaskHalSession = true,
} = {}) {
const profile = state.profile || {};
const profileId = profile.id || state.machineProfile || "unknown";
const supportedProfile = profile.rtcpProof !== false && Boolean(profile.kinematicsModuleId);
const fail = (operatorMessage, detail = {}) => ({
apiName: "web-rtcp-5axis-run-preconditions",
ok: false,
operatorMessage,
profileId,
iniPath: profile.iniPath || null,
coordinates: normalizeCoordinates(profile.traj?.coordinates),
kinematicsModuleId: profile.kinematicsModuleId || state.machineProfile || null,
...detail,
});
if (!supportedProfile) {
return fail(`run blocked: unsupported five-axis profile ${profileId}`);
}
if (!profile.iniPath || state.iniConfigReadiness?.loaded !== true || state.iniConfigReadiness?.ready !== true) {
return fail("run blocked: LinuxCNC INI not loaded");
}
if (state.linuxCncIniConfig?.path && state.linuxCncIniConfig.path !== profile.iniPath) {
return fail("run blocked: machine profile and INI path mismatch", {
iniPath: state.linuxCncIniConfig.path,
expectedIniPath: profile.iniPath,
});
}
const profileCoordinates = normalizeCoordinates(profile.traj?.coordinates);
const iniCoordinates = normalizeCoordinates(state.iniConfigReadiness?.coordinates || state.linuxCncIniConfig?.traj?.coordinates);
if (!profileCoordinates || !iniCoordinates || profileCoordinates !== iniCoordinates) {
return fail("run blocked: machine profile and INI coordinates mismatch", {
coordinates: iniCoordinates || null,
expectedCoordinates: profileCoordinates || null,
});
}
const profileKinematicsModuleId = profile.kinematicsModuleId || state.machineProfile || null;
const iniKinematicsModuleId = state.linuxCncIniConfig?.kinematicsModuleId || profileKinematicsModuleId;
if (!profileKinematicsModuleId || profileKinematicsModuleId !== iniKinematicsModuleId) {
return fail("run blocked: machine profile and INI kinematics mismatch", {
kinematicsModuleId: profileKinematicsModuleId,
expectedKinematicsModuleId: iniKinematicsModuleId,
});
}
const kinematicsReadiness = state.kinematicsRuntimeReadiness || {};
const runtimeKinematicsModuleId = kinematicsReadiness.moduleId || state.kinematicsRuntime?.moduleId || null;
if (kinematicsReadiness.loaded !== true || state.kinematicsRuntime?.loaded !== true) {
return fail("run blocked: LinuxCNC kinematics runtime not ready", {
kinematicsModuleId: profileKinematicsModuleId,
runtimeKinematicsModuleId,
});
}
if (runtimeKinematicsModuleId !== profileKinematicsModuleId) {
return fail("run blocked: LinuxCNC kinematics module mismatch", {
kinematicsModuleId: profileKinematicsModuleId,
runtimeKinematicsModuleId,
});
}
if (state.rtcpFrame?.sourceMode !== "source-derived-kinematics-wasm") {
return fail("run blocked: LinuxCNC kinematics frame not ready", {
frameSourceMode: state.rtcpFrame?.sourceMode || state.frameSourceMode || null,
});
}
if (state.machineFileStaging?.status !== "staged" || !state.machineFileStaging?.save?.files?.length) {
return fail("run blocked: LinuxCNC machine files not staged");
}
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
return fail("run blocked: no machine-file G-code opened for task/HAL session");
}
if (requireTaskHalRuntime) {
const taskHalReadiness = state.taskHalRuntimeReadiness || {};
const taskHalReady = state.taskHalRuntime?.loaded === true
&& taskHalReadiness.taskRuntimeReady === true
&& taskHalReadiness.motionRuntimeReady === true
&& taskHalReadiness.halRuntimeReady === true;
if (!taskHalReady) {
return fail("run blocked: task/HAL runtime not ready");
}
}
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (requireTaskHalSession) {
if (!state.taskHalSession?.programPath || !expectedProgramPath) {
return fail("run blocked: no machine-file G-code opened for task/HAL session");
}
if (state.taskHalSession.programPath !== expectedProgramPath) {
return fail("run blocked: task/HAL session program mismatch", {
programPath: state.taskHalSession.programPath,
expectedProgramPath,
});
}
}
return {
apiName: "web-rtcp-5axis-run-preconditions",
ok: true,
operatorMessage: null,
profileId,
machineType: profile.title || null,
iniPath: profile.iniPath,
coordinates: profileCoordinates,
kinematicsModuleId: profileKinematicsModuleId,
selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel,
programPath: expectedProgramPath,
sourceMode: "linuxcnc-task-motion-hal-wasm",
semanticBoundary: "linuxcnc_ini_profile_kinematics_task_hal_run_preconditions",
};
}
function expectedTaskHalProgramPathForState(state = {}) {
const sourceRel = state.machineFileStaging?.selectedGcodeSourceRel;
if (!sourceRel || !state.machineFileStaging?.plan || !state.machineFileStaging?.save) return null;
try {
return selectMachineFileProgram(
state.machineFileStaging.plan,
state.machineFileStaging.save,
sourceRel,
).wasmProgramPath || null;
} catch {
return null;
}
}
function deriveTaskHalCyclePeriods(state = {}) {
const taskCycleTimeSeconds = Number(state.linuxCncIniConfig?.task?.cycleTimeSeconds);
const iniTaskPeriodNs = Number.isFinite(taskCycleTimeSeconds) && taskCycleTimeSeconds > 0
? Math.round(taskCycleTimeSeconds * 1_000_000_000)
: null;
const iniServoPeriodNs = Number(state.linuxCncIniConfig?.emcmot?.servoPeriodNs);
return {
taskPeriodNs: iniTaskPeriodNs || 10000000,
servoPeriodNs: Number.isFinite(iniServoPeriodNs) && iniServoPeriodNs > 0
? Math.round(iniServoPeriodNs)
: 1000000,
};
}
function normalizeCoordinates(value) {
return String(value || "").replace(/[^A-Za-z]/g, "").toUpperCase();
}
function applyTaskHalStatusPatch(state, status, operatorMessage, {
loopSequence = null,
preserveMachine = null,
preserveAxisPose = null,
} = {}) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const statusInterpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const statusMotionPaused = ui.motionPaused === true || motion.paused === true;
const statusTaskPaused = ui.taskPaused === true || task.taskPaused === true;
const statusPaused = statusInterpState === "paused" || statusMotionPaused || statusTaskPaused;
const rawTaskState = normalizeTaskHalTaskState(ui.taskState || task.state);
const taskState = preserveMachine?.powerOn && rawTaskState === "estop-reset"
? "on"
: rawTaskState;
const taskMode = statusPaused
? programControlModeForMachine(state.machine)
: normalizeLinuxCncTaskMode(
preserveMachine?.mode ||
(preserveMachine?.allHomed && state.machine.mode !== "manual" ? state.machine.mode : null) ||
ui.taskMode ||
task.mode ||
state.machine.mode,
);
const manualPanel = taskMode === "manual"
? (preserveMachine?.manualPanel || state.machine.manualPanel || "manual")
: null;
const interpState = statusPaused
? "paused"
: Object.hasOwn(preserveMachine || {}, "interpState")
? normalizeTaskHalInterpState(preserveMachine.interpState)
: statusInterpState;
const allHomed = Boolean(
task.allHomed === true ||
ui.allHomed === true ||
preserveMachine?.allHomed ||
state.machine.allHomed,
);
const homed = normalizeHomedArrayForState(
state,
ui.homed || task.homed || preserveMachine?.homed || state.machine.homed,
allHomed,
);
const homing = Object.hasOwn(task, "homing")
? task.homing === true
: Object.hasOwn(ui, "homing")
? ui.homing === true
: Boolean(preserveMachine?.homing);
const homeState = normalizeTaskHalHomeState(
ui.homeState ||
task.homeState ||
preserveMachine?.homeState ||
state.machine.homeState ||
(allHomed ? "homed" : "unhomed"),
);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const motionPaused = statusMotionPaused || preserveMachine?.motionPaused === true;
const taskPaused = statusTaskPaused || preserveMachine?.taskPaused === true;
const paused = interpState === "paused" || motionPaused || taskPaused;
const singleStepping = ui.singleStepping === true ||
task.singleStepping === true ||
preserveMachine?.singleStepping === true;
const motionStepping = ui.motionStepping === true ||
motion.stepping === true ||
preserveMachine?.motionStepping === true;
const interpResumeState = normalizeTaskHalInterpState(
ui.interpResumeState ||
task.interpResumeState ||
(paused ? state.machine.interpResumeState || "reading" : interpState),
);
const displayPauseLock = paused && state.taskHalPauseLock?.active === true
? state.taskHalPauseLock
: null;
const axisPose = displayPauseLock?.axisPose
? clampAxisPoseToProfile(displayPauseLock.axisPose, state.profile)
: preserveAxisPose
? clampAxisPoseToProfile({ ...state.axisPose, ...preserveAxisPose }, state.profile)
: resolveTaskHalAxisPose(state, status, { paused, runState: state.runState });
const currentVelocity = paused
? 0
: Number.isFinite(ui.currentVelocity)
? Math.max(ui.currentVelocity, 0)
: state.feed.currentVelocity;
const aborted = motion.aborted === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= openedProgramLineCount;
const runState = aborted
? "stopped"
: paused
? "paused"
: taskMode === "mdi"
? "mdi"
: interpState === "reading"
? "running"
: programComplete
? "complete"
: state.runState === "jogging"
? "jogging"
: "idle";
const runtimeFeedback = createTaskHalRuntimeFeedback(state, status, axisPose, activeLine);
const shouldTrackProgramPlayback = runState === "running"
|| runState === "mdi"
|| (runState === "complete" && (state.runState === "running" || state.runState === "mdi" || state.runState === "complete"));
const taskHalSampleIndex = displayPauseLock
? clampNumber(
displayPauseLock.sampleIndex,
0,
Math.max(Number(state.programAxisPreviewPath?.samples?.length || state.programAxisPreviewPath?.sampleCount || 1) - 1, 0),
)
: shouldTrackProgramPlayback
? resolveRuntimeSampleIndexForTaskHalPose(state, axisPose, activeLine, runtimeFeedback.sampleIndex)
: clampNumber(
state.programExecutionSampleIndex || 0,
0,
Math.max(Number(state.programAxisPreviewPath?.samples?.length || state.programAxisPreviewPath?.sampleCount || 1) - 1, 0),
);
const idleRuntimeFeedback = {
...runtimeFeedback,
sampleIndex: taskHalSampleIndex,
motionIndex: displayPauseLock
? Number(displayPauseLock.motionIndex || 0)
: Number(state.programExecutionMotionIndex || runtimeFeedback.motionIndex || 0),
...(paused ? {
axisPose,
tcp: {
x: Number(displayPauseLock?.tcpPose?.x ?? state.tcpPose?.x ?? axisPose.x ?? 0),
y: Number(displayPauseLock?.tcpPose?.y ?? state.tcpPose?.y ?? axisPose.y ?? 0),
z: Number(displayPauseLock?.tcpPose?.z ?? state.tcpPose?.z ?? axisPose.z ?? 0),
},
} : {}),
};
const taskHalPlaybackPatch = shouldTrackProgramPlayback
? applyProgramPlaybackUiPatch(state, {
activeLine,
axisPose,
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
motionIndex: displayPauseLock ? Number(displayPauseLock.motionIndex || 0) : runtimeFeedback.motionIndex,
sampleIndex: taskHalSampleIndex,
runtimeFeedback: {
...runtimeFeedback,
sampleIndex: taskHalSampleIndex,
motionIndex: displayPauseLock ? Number(displayPauseLock.motionIndex || 0) : runtimeFeedback.motionIndex,
axisPose,
currentVelocityMmPerMin: paused ? 0 : runtimeFeedback.currentVelocityMmPerMin,
requestedVelocityMmPerMin: paused ? 0 : runtimeFeedback.requestedVelocityMmPerMin,
},
preferRuntimeAxisPose: true,
})
: {
activeLine: state.activeLine,
axisPose,
kinsType: kinsType || state.kinsType,
rtcpState: kinsType ? rtcpStateFromKinsType(kinsType) : state.rtcpState,
toolAxisVector: state.toolAxisVector,
programExecutionMotionIndex: Number(state.programExecutionMotionIndex || 0),
programExecutionSampleIndex: taskHalSampleIndex,
programRuntimeFeedback: idleRuntimeFeedback,
programUiExecution: state.programUiExecution
? createProgramUiExecution({
...state,
axisPose,
tcpPose: displayPauseLock?.tcpPose || state.tcpPose,
rtcpFrame: displayPauseLock?.rtcpFrame || state.rtcpFrame,
kinsType: kinsType || state.kinsType,
programExecutionSampleIndex: taskHalSampleIndex,
programRuntimeFeedback: idleRuntimeFeedback,
}, { preferRuntimePose: true })
: state.programUiExecution,
};
const loopActive = state.taskHalStatusLoop?.active === true
&& loopSequence !== null
&& Number(state.taskHalStatusLoop.sequence) === Number(loopSequence)
&& (runState === "running" || runState === "mdi" || runState === "paused");
const nextTickCount = loopActive ? Number(state.taskHalStatusLoop.tickCount || 0) + 1 : Number(state.taskHalStatusLoop?.tickCount || 0);
const feedbackHistory = [taskHalPlaybackPatch.programRuntimeFeedback, ...(state.programRuntimeFeedbackHistory || [])].slice(0, 100);
const lineStatus = runState === "complete"
? "done"
: runState === "running" || runState === "mdi"
? "running"
: runState;
return {
taskHalStatus: status,
taskHalExecutionPending: false,
taskHalPauseLock: displayPauseLock ? { ...displayPauseLock, lastStatusAt: new Date().toISOString() } : null,
taskHalFallbackReason: null,
pendingJogCommand: null,
...taskHalPlaybackPatch,
...(displayPauseLock ? {
tcpPose: displayPauseLock.tcpPose,
rtcpFrame: displayPauseLock.rtcpFrame,
dro: displayPauseLock.dro,
} : {}),
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
machine: {
...state.machine,
powerOn: taskState === "on",
estopActive: taskState === "estop",
motionEnabled: taskState === "on",
taskState,
mode: taskMode,
manualPanel,
interpState,
interpResumeState,
taskPaused: paused,
motionPaused,
singleStepping,
motionStepping,
allHomed,
homed,
homing,
homeState,
noForceHoming: Boolean(state.machine.noForceHoming),
},
runState,
taskHalStatusLoop: loopSequence === null
? state.taskHalStatusLoop
: {
...state.taskHalStatusLoop,
active: loopActive,
tickCount: nextTickCount,
lastStatusAt: new Date().toISOString(),
stopReason: loopActive ? null : runState,
},
feed: {
...state.feed,
currentVelocity,
},
programRuntimeFeedbackHistory: feedbackHistory,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, taskHalPlaybackPatch.programRuntimeFeedback, {
status: lineStatus,
source: "linuxcnc-task-motion-hal-wasm",
}),
operatorMessage,
};
}
function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode);
const aborted = motion.aborted === true;
const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
task.taskPaused === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const complete = interpState === "idle"
&& Number(task.nextProgramLine || 0) >= openedProgramLineCount;
if (paused) {
return state.runState === "paused" ||
state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true ||
state.machine?.motionPaused === true;
}
return !aborted && !paused && !complete && (interpState === "reading" || taskMode === "mdi");
}
function isTaskHalStatusPaused(status = {}) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
return normalizeTaskHalInterpState(ui.interpState || task.interpState) === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
task.taskPaused === true;
}
function isTaskHalRunPausedByOperator(state = {}) {
return state.runState === "paused"
|| state.machine?.interpState === "paused"
|| state.machine?.taskPaused === true
|| state.machine?.motionPaused === true;
}
function programControlModeForMachine(machine = {}) {
return machine.mode === "mdi" ? "mdi" : "auto";
}
function createPowerToggleMachinePatch(machine = {}, turningOn = false) {
const poweredOffHomed = Array.isArray(machine.homed)
? machine.homed.map(() => false)
: [false, false, false, false, false];
return {
...machine,
powerOn: turningOn,
estopActive: false,
motionEnabled: turningOn,
taskState: turningOn ? "on" : "estop-reset",
manualPanel: "manual",
allHomed: turningOn ? Boolean(machine.allHomed) : false,
homed: turningOn ? normalizeHomedArrayForMachine(machine, Boolean(machine.allHomed)) : poweredOffHomed,
homing: false,
homeState: turningOn ? (machine.allHomed ? "homed" : machine.homeState || "unhomed") : "unhomed",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
};
}
function createTaskHalPauseLock(state = {}, source = "pause") {
return {
apiName: "web-rtcp-5axis-task-hal-pause-lock",
active: true,
source,
createdAt: new Date().toISOString(),
runState: "paused",
interpState: "paused",
sampleIndex: Number(state.programExecutionSampleIndex || 0),
motionIndex: Number(state.programExecutionMotionIndex || 0),
activeLine: Number(state.activeLine || state.programStartLine || 1),
axisPose: clampAxisPoseToProfile(state.axisPose || initialAxisPose, state.profile),
tcpPose: {
x: Number(state.tcpPose?.x || 0),
y: Number(state.tcpPose?.y || 0),
z: Number(state.tcpPose?.z || 0),
},
rtcpFrame: state.rtcpFrame,
dro: state.dro,
semanticBoundary: "linuxcnc_axis_task_pauseresume_freezes_task_hal_status_until_resume",
};
}
function zeroProgramRuntimeVelocity(feedback) {
if (!feedback) return feedback;
return {
...feedback,
currentVelocityMmPerMin: 0,
requestedVelocityMmPerMin: 0,
distanceToGo: 0,
dtg: feedback.dtg ? { ...feedback.dtg, x: 0, y: 0, z: 0 } : feedback.dtg,
activeDepth: 0,
};
}
function createStoppedProgramStatePatch(state, {
reason = "stopped",
operatorMessage = "program stopped",
} = {}) {
return {
machine: {
...state.machine,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
motionPaused: false,
singleStepping: false,
motionStepping: false,
},
runState: reason === "aborted" ? "stopped" : reason,
taskHalPauseLock: null,
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
sequence: Number(state.taskHalStatusLoop?.sequence || 0) + 1,
stopReason: reason,
lastStatusAt: new Date().toISOString(),
},
feed: {
...state.feed,
currentVelocity: 0,
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage,
};
}
function resolveTaskHalKinsType(state, status, activeLine) {
const ui = status?.ui || {};
const numeric = Number(ui.switchkinsType);
if (Number.isFinite(numeric) && numeric !== 0) {
return kinsTypeFromSwitchkinsTypeValue(state, numeric);
}
const programKinsType = kinsTypeFromProgramActiveLine(state, activeLine);
if (programKinsType) {
return programKinsType;
}
return kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
}
function kinsTypeFromProgramActiveLine(state, activeLine) {
const motion = programMotionAtOrBeforeLine(state, activeLine)
|| state.programExecution?.motion?.[clampMotionIndex(state, state.programExecutionMotionIndex)];
return kinsTypeFromProgramMotion(state, motion);
}
function programMotionAtOrBeforeLine(state, activeLine) {
const motion = state.programExecution?.motion;
if (!Array.isArray(motion) || motion.length === 0) return null;
const line = Number(activeLine);
if (!Number.isFinite(line)) return null;
let candidate = null;
for (const item of motion) {
const itemLine = Number(item?.line);
if (!Number.isFinite(itemLine)) continue;
if (itemLine > line) break;
candidate = item;
}
return candidate;
}
function resolveTaskHalAxisPose(state, status, { paused = false, runState = state.runState } = {}) {
const ui = status?.ui || {};
const axisPose = ui.axisPose;
if (!axisPose || typeof axisPose !== "object") {
return state.axisPose;
}
if (state.pendingJogCommand && (ui.axisPoseFrame === "task-local" || isJogStatus(status))) {
const { axis, direction, increment, basePose } = state.pendingJogCommand;
return clampAxisPoseToProfile({
...basePose,
[axis]: Number(basePose?.[axis] || 0) + direction * increment,
}, state.profile);
}
if (ui.axisPoseFrame === "work") {
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
}
if (ui.axisPoseDelta && typeof ui.axisPoseDelta === "object") {
return addAxisDelta(state.axisPose, ui.axisPoseDelta, state.profile);
}
if (!paused && runState !== "running" && runState !== "mdi" && !isJogStatus(status)) {
return state.axisPose;
}
if (!ui.axisPoseFrame && wouldResetNonZeroPoseToLocalZero(state.axisPose, axisPose)) {
return state.axisPose;
}
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
}
function addAxisDelta(axisPose, delta, profile) {
const next = { ...axisPose };
for (const axis of ["x", "y", "z", "a", "b", "c"]) {
if (!Number.isFinite(Number(delta[axis]))) continue;
next[axis] = Number(next[axis] || 0) + Number(delta[axis]);
}
return clampAxisPoseToProfile(next, profile);
}
function isJogStatus(status) {
const motion = status?.motionStatus?.motion || {};
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
}
function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
const axes = ["x", "y", "z", "a", "b", "c"];
const currentHasNonZero = axes.some((axis) => Math.abs(Number(currentPose[axis] || 0)) > 0.001);
const nextIsNearZero = axes.every((axis) => Math.abs(Number(nextPose[axis] || 0)) <= 0.001);
return currentHasNonZero && nextIsNearZero;
}
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const ui = status?.ui || {};
const motion = status?.motionStatus?.motion || {};
const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0);
const motionProgramLine = Number(motion.programLine || 0);
const interpState = normalizeTaskHalInterpState(ui.interpState || status?.task?.interpState);
const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
status?.task?.taskPaused === true;
const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0);
const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60;
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "linuxcnc-task-motion-hal-wasm",
semanticBoundary: status?.semanticBoundary || "linuxcnc_task_motion_hal_wasm_simulation_runtime",
sampleIndex: Number(ui.servoCycle || 0),
motionIndex: Math.max(Number(ui.activeLine || 1) - 1, 0),
line: activeLine,
motionProgramLine,
halProgramLine,
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine,
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
paused,
timeSeconds: Number(ui.taskCycle || 0) * 0.01,
axisPose,
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: requestedVelocity,
distanceToGo: motion.inPosition === true ? 0 : 1,
dtg: { x: 0, y: 0, z: 0 },
queueDepth: Number(ui.motionQueueDepth || status?.motionStatus?.commandQueueDepth || 0),
activeDepth: motion.inPosition === true ? 0 : 1,
cycle: Number(ui.servoCycle || status?.servoCycle || 0),
taskCycle: Number(ui.taskCycle || status?.task?.cycle || 0),
halChangedPinCount: Number(ui.halChangedPinCount || 0),
};
}
function buildTaskHalProgramMotionPlanFromPreviewPath({
programPath = null,
path = null,
profile = defaultProfile,
linearUnits = "mm",
} = {}) {
const samples = path?.samples;
if (!Array.isArray(samples) || samples.length < 2) return null;
const samplePeriodSeconds = Math.max(Number(path.samplePeriodMs || 50) / 1000, 0.001);
const segments = [];
for (let index = 1; index < samples.length; index += 1) {
const startSample = samples[index - 1];
const endSample = samples[index];
const startAxes = taskHalPlanAxesFromPose(axisPoseFromProgramPathSample(startSample, initialAxisPose, profile));
const endAxes = taskHalPlanAxesFromPose(axisPoseFromProgramPathSample(endSample, startAxes, profile));
const startSeconds = Number.isFinite(Number(startSample?.timeMs))
? Number(startSample.timeMs) / 1000
: (index - 1) * samplePeriodSeconds;
const endSeconds = Number.isFinite(Number(endSample?.timeMs))
? Number(endSample.timeMs) / 1000
: index * samplePeriodSeconds;
const durationSeconds = Math.max(endSeconds - startSeconds, samplePeriodSeconds);
const velocityMmPerMin = estimatePreviewSegmentVelocityMmPerMin(startAxes, endAxes, durationSeconds);
const type = endSample?.motionType || startSample?.motionType || "STRAIGHT_FEED";
segments.push({
line: Number(endSample?.line || startSample?.line || 1),
type,
motionClass: String(type).includes("TRAVERSE") ? "rapid" : "feed",
feedMode: "units-per-minute",
startSeconds,
durationSeconds,
elapsedSeconds: startSeconds + durationSeconds,
feedRate: velocityMmPerMin,
linearUnits,
velocityMmPerMin,
requestedVelocityMmPerMin: velocityMmPerMin,
startAxes,
endAxes,
});
}
if (segments.length === 0) return null;
return {
apiName: "web-rtcp-5axis-task-hal-program-motion-plan",
semanticBoundary: "linuxcnc_task_hal_plan_from_expanded_axis_preview_samples",
programPath,
linearUnits,
totalSeconds: segments[segments.length - 1].elapsedSeconds,
segmentCount: segments.length,
segments,
};
}
function taskHalPlanAxesFromPose(pose = {}) {
return {
x: Number(pose.x || 0),
y: Number(pose.y || 0),
z: Number(pose.z || 0),
a: Number(pose.a || 0),
b: Number(pose.b || 0),
c: Number(pose.c || 0),
};
}
function estimatePreviewSegmentVelocityMmPerMin(startAxes = {}, endAxes = {}, durationSeconds = 0.05) {
const linearDistance = Math.hypot(
Number(endAxes.x || 0) - Number(startAxes.x || 0),
Number(endAxes.y || 0) - Number(startAxes.y || 0),
Number(endAxes.z || 0) - Number(startAxes.z || 0),
);
const rotaryDistance = Math.hypot(
Number(endAxes.a || 0) - Number(startAxes.a || 0),
Number(endAxes.b || 0) - Number(startAxes.b || 0),
Number(endAxes.c || 0) - Number(startAxes.c || 0),
);
const distance = Math.max(linearDistance, rotaryDistance * 0.1, 0.001);
return durationSeconds > 0 ? (distance / durationSeconds) * 60 : 1;
}
function resolveRuntimeSampleIndexForLine(state = {}, activeLine, fallbackSampleIndex = 0) {
const samples = state.programAxisPreviewPath?.samples;
if (!Array.isArray(samples) || samples.length === 0) {
return Number(fallbackSampleIndex) || 0;
}
const line = Number(activeLine);
if (!Number.isFinite(line)) {
return clampNumber(fallbackSampleIndex, 0, samples.length - 1);
}
const current = clampNumber(state.programExecutionSampleIndex || 0, 0, samples.length - 1);
for (let index = current; index < samples.length; index += 1) {
if (Number(samples[index]?.line) >= line) return index;
}
return samples.length - 1;
}
function resolveRuntimeSampleIndexForTaskHalPose(state = {}, axisPose = {}, activeLine, fallbackSampleIndex = 0) {
const samples = state.programAxisPreviewPath?.samples;
if (!Array.isArray(samples) || samples.length === 0) {
return resolveRuntimeSampleIndexForLine(state, activeLine, fallbackSampleIndex);
}
const current = clampNumber(state.programExecutionSampleIndex || 0, 0, samples.length - 1);
const previousCycle = Number(state.programRuntimeFeedback?.cycle || 0);
const nextCycle = Number(fallbackSampleIndex || 0);
const cycleDelta = Number.isFinite(nextCycle) && Number.isFinite(previousCycle)
? Math.max(nextCycle - previousCycle, 0)
: 0;
const adaptiveWindow = Math.max(320, Math.min(samples.length - 1, Math.ceil(cycleDelta * 8) + 240));
const windowStart = current;
const windowEnd = Math.min(samples.length - 1, current + adaptiveWindow);
const line = Number(activeLine);
let bestIndex = current;
let bestScore = Number.POSITIVE_INFINITY;
for (let index = windowStart; index <= windowEnd; index += 1) {
const sample = samples[index];
const samplePose = axisPoseFromProgramPathSample(sample, axisPose, state.profile);
const linePenalty = Number.isFinite(line) && Number.isFinite(Number(sample?.line))
? Math.min(Math.abs(Number(sample.line) - line), 12) * 0.25
: 0;
const forwardPenalty = (index - current) * 0.000001;
const score = taskHalPoseDistanceScore(axisPose, samplePose) + linePenalty + forwardPenalty;
if (score < bestScore) {
bestScore = score;
bestIndex = index;
}
}
return Math.max(current, bestIndex);
}
function taskHalPoseDistanceScore(a = {}, b = {}) {
const linear = ["x", "y", "z"].reduce((sum, axis) => {
const delta = Number(a[axis] || 0) - Number(b[axis] || 0);
return sum + delta * delta;
}, 0);
const rotary = ["a", "b", "c"].reduce((sum, axis) => {
const delta = Number(a[axis] || 0) - Number(b[axis] || 0);
return sum + (delta * delta * 0.01);
}, 0);
return linear + rotary;
}
function applyProgramPlaybackUiPatch(state = {}, {
activeLine,
axisPose,
kinsType,
rtcpState,
motionIndex,
sampleIndex,
runtimeFeedback,
preferRuntimeAxisPose = false,
} = {}) {
const sample = currentProgramPathSample(state, sampleIndex);
const runtimeAxisPose = clampAxisPoseToProfile(axisPose || runtimeFeedback?.axisPose || state.axisPose, state.profile);
const sampleAxisPose = preferRuntimeAxisPose
? runtimeAxisPose
: axisPoseFromProgramPathSample(sample, runtimeAxisPose, state.profile);
const sampleKinsType = sample?.activeKinematics
? normalizeSampleKinsType(state, sample.activeKinematics)
: kinsType;
const sampleLine = Number(sample?.line || activeLine || state.activeLine || 0);
const sampleMotionIndex = Number.isFinite(Number(sample?.segmentIndex))
? Number(sample.segmentIndex)
: Number(motionIndex || 0);
const sampleSampleIndex = Number.isFinite(Number(sample?.sampleIndex))
? Number(sample.sampleIndex)
: Number(sampleIndex || 0);
const toolAxisVector = toolAxisVectorFromSample(sample, state.toolAxisVector);
const enrichedFeedback = enrichRuntimeFeedbackWithSample(runtimeFeedback, sample, {
...state,
activeLine: sampleLine,
axisPose: sampleAxisPose,
kinsType: sampleKinsType || state.kinsType,
toolAxisVector,
}, { preferRuntimeAxisPose });
const patchState = {
...state,
activeLine: sampleLine,
axisPose: sampleAxisPose,
kinsType: sampleKinsType || state.kinsType,
rtcpState: sampleKinsType ? rtcpStateFromKinsType(sampleKinsType) : rtcpState || state.rtcpState,
toolAxisVector,
programExecutionMotionIndex: sampleMotionIndex,
programExecutionSampleIndex: sampleSampleIndex,
programRuntimeFeedback: enrichedFeedback,
};
return {
activeLine: patchState.activeLine,
axisPose: patchState.axisPose,
kinsType: patchState.kinsType,
rtcpState: patchState.rtcpState,
toolAxisVector,
programExecutionMotionIndex: patchState.programExecutionMotionIndex,
programExecutionSampleIndex: patchState.programExecutionSampleIndex,
programRuntimeFeedback: enrichedFeedback,
programUiExecution: createProgramUiExecution(patchState, { preferRuntimePose: preferRuntimeAxisPose }),
};
}
function createProgramUiExecution(state = {}, {
preferRuntimePose = false,
} = {}) {
const sample = currentProgramPathSample(state, state.programExecutionSampleIndex);
const feedback = state.programRuntimeFeedback || {};
const sampleCount = Number(state.programAxisPreviewPath?.sampleCount || state.programAxisPreviewPath?.samples?.length || 0);
const lineExecutionTrace = state.programAxisPreviewPath?.lineExecutionTrace || [];
const traceEntry = sample
? lineExecutionTrace.find((entry) => (
Number(entry.segmentIndex) === Number(sample.segmentIndex)
&& entry.sourceFile === sample.sourceFile
&& Number(entry.line) === Number(sample.line)
)) || null
: null;
const gcodeSteps = state.programAxisPreviewPath?.gcodeExecutionProcess?.executionSteps || [];
const gcodeStep = traceEntry
? gcodeSteps.find((step) => Number(step.result?.traceExecutionIndex) === Number(traceEntry.executionIndex)) || null
: null;
const sourceFile = sample?.sourceFile || traceEntry?.sourceFile || feedback.sourceFile || null;
const sourceLine = Number(sample?.line || traceEntry?.line || feedback.line || state.activeLine || 0);
const sampleIndex = Number(sample?.sampleIndex ?? state.programExecutionSampleIndex ?? 0);
return {
apiName: "web-rtcp-5axis-ui-gcode-live-execution",
status: state.runState || "idle",
source: sample
? "programAxisPreviewPath.samples"
: feedback.sourceMode || state.programExecutionSourceMode || "ui-state",
samplePeriodMs: Number(state.programAxisPreviewPath?.samplePeriodMs || 0),
sampleIndex,
sampleCount,
executedSampleCount: sampleCount > 0 ? Math.min(sampleIndex + 1, sampleCount) : 0,
timeMs: Number(sample?.timeMs ?? feedback.timeSeconds * 1000 ?? 0),
activeProgram: state.activeProgram || null,
sourceFile,
line: sourceLine,
statement: sample?.statement || traceEntry?.statement || gcodeStep?.statement || "",
operation: traceEntry?.operation || gcodeStep?.result?.operation || null,
executionIndex: traceEntry?.executionIndex ?? null,
gcodeStepIndex: gcodeStep?.stepIndex ?? null,
segmentIndex: sample?.segmentIndex ?? traceEntry?.segmentIndex ?? null,
motionType: sample?.motionType || traceEntry?.motionType || feedback.motionType || feedback.type || null,
activeKinematics: sample?.activeKinematics || traceEntry?.activeKinematicsAfter || state.kinsType,
joint: preferRuntimePose
? pickExecutionAxes(feedback.axisPose || state.axisPose || {})
: sample?.joint || pickExecutionAxes(state.axisPose || {}),
tcp: preferRuntimePose && feedback.tcp
? feedback.tcp
: sample?.tcp || {
x: Number(state.tcpPose?.x || state.axisPose?.x || 0),
y: Number(state.tcpPose?.y || state.axisPose?.y || 0),
z: Number(state.tcpPose?.z || state.axisPose?.z || 0),
},
toolAxis: sample?.toolAxis || toolAxisSampleFromVector(state.toolAxisVector),
tool: sample?.tool || currentPathTool(state),
machineState: sample?.machineState || null,
semanticBoundary: "ui_updates_from_real_expanded_linuxcnc_gcode_sample_stream",
};
}
function currentProgramPathSample(state = {}, sampleIndex = 0) {
const samples = state.programAxisPreviewPath?.samples;
if (!Array.isArray(samples) || samples.length === 0) return null;
const index = clampNumber(sampleIndex, 0, samples.length - 1);
return samples[index] || null;
}
function axisPoseFromProgramPathSample(sample, fallbackPose = {}, profile = defaultProfile) {
const joint = sample?.joint || {};
return clampAxisPoseToProfile({
...fallbackPose,
x: numberOrFallback(joint.x, fallbackPose.x, 0),
y: numberOrFallback(joint.y, fallbackPose.y, 0),
z: numberOrFallback(joint.z, fallbackPose.z, 0),
b: numberOrFallback(joint.b, fallbackPose.b, 0),
c: numberOrFallback(joint.c, fallbackPose.c, 0),
}, profile || defaultProfile);
}
function normalizeSampleKinsType(state = {}, value) {
if (value === "tcp-xyzbc" || value === "tcp-xyzac" || value === "identity" || value === "userk") {
return value;
}
if (value === "tcp") return tcpKinsTypeForProfile(state.profile) || value;
return value || null;
}
function toolAxisVectorFromSample(sample, fallbackVector = {}) {
const axis = sample?.toolAxis || {};
return {
x: numberOrFallback(axis.x, axis.i, fallbackVector.x, 0),
y: numberOrFallback(axis.y, axis.j, fallbackVector.y, 0),
z: numberOrFallback(axis.z, axis.k, fallbackVector.z, 1),
};
}
function toolAxisSampleFromVector(vector = {}) {
return {
i: Number(vector.x || 0),
j: Number(vector.y || 0),
k: Number(vector.z ?? 1),
};
}
function enrichRuntimeFeedbackWithSample(feedback = {}, sample = null, state = {}, {
preferRuntimeAxisPose = false,
} = {}) {
if (!sample) return feedback;
const runtimeAxisPose = feedback?.axisPose || state.axisPose || {};
const tcp = preferRuntimeAxisPose
? {
x: Number(runtimeAxisPose.x ?? 0),
y: Number(runtimeAxisPose.y ?? 0),
z: Number(runtimeAxisPose.z ?? 0),
}
: sample.tcp || {
x: Number(state.tcpPose?.x ?? state.axisPose?.x ?? feedback?.axisPose?.x ?? 0),
y: Number(state.tcpPose?.y ?? state.axisPose?.y ?? feedback?.axisPose?.y ?? 0),
z: Number(state.tcpPose?.z ?? state.axisPose?.z ?? feedback?.axisPose?.z ?? 0),
};
return {
...(feedback || {}),
sampleIndex: Number(sample.sampleIndex || 0),
motionIndex: Number(sample.segmentIndex || feedback?.motionIndex || 0),
line: Number(sample.line || feedback?.line || 0),
sourceFile: sample.sourceFile || feedback?.sourceFile || null,
statement: sample.statement || feedback?.statement || "",
motionType: sample.motionType || feedback?.motionType || null,
activeKinematics: sample.activeKinematics || feedback?.activeKinematics || state.kinsType,
axisPose: preferRuntimeAxisPose
? runtimeAxisPose
: axisPoseFromProgramPathSample(sample, state.axisPose || feedback?.axisPose || {}, state.profile),
tcp,
toolAxis: sample.toolAxis || feedback?.toolAxis || null,
machineState: sample.machineState || feedback?.machineState || null,
};
}
function createProgramLineExecutionPatch(previous = {}, feedback = null, {
status = "running",
source = null,
} = {}) {
const line = Number(feedback?.line || 0);
if (!Number.isFinite(line) || line <= 0) {
return previous || {};
}
const axisPose = feedback.axisPose || {};
return {
...(previous || {}),
[line]: {
status,
source: source || feedback.sourceMode || "unknown",
line,
feed: Number(feedback.currentVelocityMmPerMin || 0),
requestedFeed: Number(feedback.requestedVelocityMmPerMin || 0),
feedMode: feedback.feedMode || null,
axisPose: pickExecutionAxes(axisPose),
taskCycle: Number(feedback.taskCycle || 0),
servoCycle: Number(feedback.cycle || 0),
sampleIndex: Number(feedback.sampleIndex || 0),
motionIndex: Number(feedback.motionIndex || 0),
updatedAt: new Date().toISOString(),
},
};
}
function pickExecutionAxes(axisPose = {}) {
return Object.fromEntries(["x", "y", "z", "a", "b", "c"].map((axis) => [
axis,
Number(axisPose[axis] || 0),
]));
}
function normalizeTaskHalTaskState(value) {
const state = String(value || "").toLowerCase().replaceAll("_", "-");
if (state === "on") return "on";
if (state === "estop") return "estop";
return "estop-reset";
}
function normalizeTaskHalInterpState(value) {
const state = String(value || "").toLowerCase();
if (state === "paused") return "paused";
if (state === "reading") return "reading";
if (state === "waiting") return "waiting";
return "idle";
}
function normalizeTaskHalHomeState(value) {
const state = String(value || "").toLowerCase().replaceAll("_", "-");
if (state === "homing") return "homing";
if (state === "homed") return "homed";
if (state === "no-force-homing") return "no-force-homing";
return "unhomed";
}
function jointCountForState(state = {}) {
return Math.max(
Number(state.profile?.jointConfig?.length || 0),
Number(state.profile?.joints?.length || 0),
Array.isArray(state.machine?.homed) ? state.machine.homed.length : 0,
5,
);
}
function createHomedArrayForState(state = {}, value = false) {
return Array.from({ length: jointCountForState(state) }, () => Boolean(value));
}
function normalizeHomedArrayForState(state = {}, source = [], allHomed = false) {
const count = jointCountForState(state);
if (allHomed) return Array.from({ length: count }, () => true);
if (!Array.isArray(source)) return Array.from({ length: count }, () => false);
return Array.from({ length: count }, (_, index) => Boolean(source[index]));
}
function normalizeHomedArrayForMachine(machine = {}, allHomed = false, fallbackCount = 5) {
const source = Array.isArray(machine.homed) ? machine.homed : [];
const count = Math.max(source.length, fallbackCount);
if (allHomed) return Array.from({ length: count }, () => true);
return Array.from({ length: count }, (_, index) => Boolean(source[index]));
}
function kinsTypeFromSwitchkinsTypeValue(state, value) {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric === 0) return "identity";
return state.profile.kinematicsParameters.switchkinsTypes
.find((type) => Number(type.value) === numeric)?.webKinsType || state.kinsType;
}
function canMoveMachine(state) {
return createLinuxCncTaskPolicyStatus(state).canMove;
}
function normalizeMachineForLinuxCncTask(machine = {}, runState = "idle") {
const baseMachine = { ...initialState.machine, ...machine };
const taskState = deriveLinuxCncTaskState(baseMachine);
const mode = normalizeLinuxCncTaskMode(baseMachine.mode);
const interpState = baseMachine.interpState
|| (runState === "running" ? "reading" : runState === "paused" || runState === "stepping" ? "paused" : "idle");
const manualPanel = mode === "manual"
? (baseMachine.manualPanel === "jog" ? "jog" : "manual")
: null;
const homed = normalizeHomedArrayForMachine(baseMachine, Boolean(baseMachine.allHomed));
const allHomed = Boolean(baseMachine.allHomed || homed.every(Boolean));
return {
...baseMachine,
taskState,
mode,
manualPanel,
interpState,
interpResumeState: baseMachine.interpResumeState || (interpState === "paused" ? "reading" : interpState),
taskPaused: Boolean(baseMachine.taskPaused || interpState === "paused"),
motionPaused: Boolean(baseMachine.motionPaused || baseMachine.taskPaused || interpState === "paused"),
singleStepping: Boolean(baseMachine.singleStepping),
motionStepping: Boolean(baseMachine.motionStepping),
resumeInhibit: Boolean(baseMachine.resumeInhibit),
motionEnabled: taskState === "on",
powerOn: taskState === "on",
estopActive: taskState === "estop",
allHomed,
homed,
noForceHoming: Boolean(baseMachine.noForceHoming),
homing: Boolean(baseMachine.homing),
homeState: allHomed ? "homed" : baseMachine.homeState || "unhomed",
};
}
function createIniConfigReadiness(iniConfig) {
return {
apiName: "web-rtcp-5axis-ini-config-readiness",
loaded: true,
ready: iniConfig.validation.ready,
path: iniConfig.path,
missing: iniConfig.validation.missing,
machineName: iniConfig.machineName,
coordinates: iniConfig.traj.coordinates,
kinematics: iniConfig.kinematics.name,
axisCount: iniConfig.validation.axisCount,
jointCount: iniConfig.validation.jointCount,
semanticBoundary: iniConfig.semanticBoundary,
};
}
function normalizeKinsTypeForProfile(kinsType, profile) {
if (kinsType !== "tcp-xyzac" && kinsType !== "tcp-xyzbc") return kinsType;
return tcpKinsTypeForProfile(profile) || "identity";
}
function profileSupportsTcp(profile) {
return Boolean(profile?.tcpCapable !== false && tcpKinsTypeForProfile(profile));
}
function tcpKinsTypeForProfile(profile) {
const types = profile?.kinematicsParameters?.switchkinsTypes || [];
return types.find((type) => type.value === 1 && String(type.webKinsType || "").startsWith("tcp-"))
?.webKinsType
|| types.find((type) => String(type.webKinsType || "").startsWith("tcp-"))
?.webKinsType
|| null;
}
function defaultKinsTypeForProfile(profile) {
return profile?.kinematicsParameters?.fixedTrtDefault
? tcpKinsTypeForProfile(profile) || "identity"
: "identity";
}
function clampAxisPoseToProfile(axisPose, profile = defaultProfile) {
const next = { ...axisPose };
for (const [axis, limits] of Object.entries(profile.axisLimits || {})) {
const key = axis.toLowerCase();
const value = Number(next[key] ?? 0);
const min = Number.isFinite(limits.min) ? limits.min : -Infinity;
const max = Number.isFinite(limits.max) ? limits.max : Infinity;
next[key] = Math.min(Math.max(value, min), max);
}
return next;
}
function homeAxisPoseForState(state = {}) {
if (state.profile?.id === "xyzac-trt" || state.profile?.id === "xyzbc-trt") {
return clampAxisPoseToProfile({
...initialAxisPose,
x: 0,
y: 0,
z: 10,
}, state.profile);
}
const pose = { ...initialAxisPose };
for (const joint of state.profile?.jointConfig || []) {
const axis = String(joint.axis || "").toLowerCase();
if (!axis) continue;
pose[axis] = Number(joint.home || 0);
}
return clampAxisPoseToProfile(pose, state.profile || defaultProfile);
}
function executeMdiCommand(state, rawCommand) {
const command = normalizeMdiCommand(rawCommand);
if (!command) {
return {
patch: {
operatorMessage: "MDI blocked: empty command",
},
};
}
const parsed = parseMdiCommand(command);
const parsedKinsType = resolveMdiKinsType(state, parsed.kinsType);
const distanceMode = parsed.distanceMode || state.machine.mdiDistanceMode || "absolute";
const nextAxisPose = { ...state.axisPose };
for (const axis of ["x", "y", "z", "a", "b", "c"]) {
if (!Number.isFinite(parsed.axes[axis])) continue;
nextAxisPose[axis] = distanceMode === "relative"
? Number(nextAxisPose[axis] || 0) + parsed.axes[axis]
: parsed.axes[axis];
}
const nextMachine = {
...state.machine,
mode: "mdi",
manualPanel: null,
mdiCommand: command,
mdiDistanceMode: distanceMode,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
};
const mdiExecution = createMdiProgramExecution(command, nextAxisPose, parsed.motionCode);
const mdiFeed = parsed.feedRate !== null
? { ...state.feed, feedRate: parsed.feedRate, currentVelocity: parsed.feedRate }
: state.feed;
const mdiTiming = buildProgramExecutionTiming({
motion: mdiExecution.motion,
profile: state.profile,
feedOverride: mdiFeed.feedOverride,
rapidOverride: mdiFeed.rapidOverride,
defaultFeedRate: mdiFeed.feedRate,
});
const mdiTimingSnapshot = timingAtMotionIndex(mdiTiming, 0);
const patch = {
machine: nextMachine,
runState: "mdi",
axisPose: nextAxisPose,
activeProgram: "MDI",
programSource: "operator-mdi",
programStartLine: 1,
activeLine: 1,
lineCount: 1,
fileSizeBytes: command.length,
programLines: [command],
programExecution: mdiExecution,
programExecutionTiming: mdiTiming,
programExecutionSourceMode: "operator-mdi",
programExecutionMotionIndex: 0,
programElapsedSeconds: mdiTimingSnapshot.elapsedSeconds,
programRemainingSeconds: mdiTimingSnapshot.remainingSeconds,
preview: {
...state.preview,
pathPoints: hasMdiAxisWords(parsed) ? Math.max(state.preview.pathPoints, 2) : state.preview.pathPoints,
},
feed: { ...mdiFeed, currentVelocity: mdiTimingSnapshot.currentVelocity || mdiFeed.currentVelocity },
spindle: parsed.spindleRpm !== null || parsed.spindleEnabled !== null
? {
...state.spindle,
rpm: parsed.spindleRpm ?? state.spindle.rpm,
enabled: parsed.spindleEnabled ?? state.spindle.enabled,
direction: parsed.spindleDirection ?? state.spindle.direction,
halPins: spindleHalPinsForState({
...state.spindle,
rpm: parsed.spindleRpm ?? state.spindle.rpm,
enabled: parsed.spindleEnabled ?? state.spindle.enabled,
direction: parsed.spindleDirection ?? state.spindle.direction,
}),
}
: state.spindle,
coolant: parsed.coolantPatch
? { ...state.coolant, ...parsed.coolantPatch }
: state.coolant,
kinsType: parsedKinsType || state.kinsType,
rtcpState: parsedKinsType?.startsWith("tcp-") ? "on" : parsedKinsType ? "off" : state.rtcpState,
mdiHistory: [command, ...(state.mdiHistory || []).filter((entry) => entry !== command)].slice(0, 8),
operatorMessage: `MDI ${command}`,
};
return { patch };
}
function createManualTouchOffMdiPatch(state, patch, command) {
return {
...patch,
machine: {
...patch.machine,
mode: "manual",
manualPanel: state.machine.manualPanel || "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: "idle",
operatorMessage: `manual touch off ${command}`,
};
}
function normalizeMdiCommand(command) {
return String(command || "")
.replace(/\([^)]*\)/g, " ")
.replace(/;.*$/g, " ")
.trim()
.replace(/\s+/g, " ")
.toUpperCase();
}
function parseMdiCommand(command) {
const parsed = {
axes: {},
feedRate: null,
spindleRpm: null,
spindleEnabled: null,
spindleDirection: null,
coolantPatch: null,
distanceMode: null,
motionCode: null,
kinsType: null,
};
const words = [...command.matchAll(/([A-Z])\s*([-+]?\d+(?:\.\d+)?)/g)]
.map((match) => ({ letter: match[1], value: Number(match[2]) }));
for (const word of words) {
if (["X", "Y", "Z", "A", "B", "C"].includes(word.letter)) {
parsed.axes[word.letter.toLowerCase()] = word.value;
continue;
}
if (word.letter === "F") {
parsed.feedRate = Math.max(0, word.value);
continue;
}
if (word.letter === "S") {
parsed.spindleRpm = Math.max(0, word.value);
continue;
}
if (word.letter === "G") {
if (word.value === 90) parsed.distanceMode = "absolute";
if (word.value === 91) parsed.distanceMode = "relative";
if ([0, 1, 2, 3].includes(word.value)) parsed.motionCode = `G${word.value}`;
continue;
}
if (word.letter === "M") {
applyMdiMCode(parsed, word.value);
}
}
return parsed;
}
function applyMdiMCode(parsed, value) {
if (value === 3 || value === 4) {
parsed.spindleEnabled = true;
parsed.spindleDirection = value === 4 ? "reverse" : "forward";
} else if (value === 5) {
parsed.spindleEnabled = false;
parsed.spindleDirection = "stop";
} else if (value === 7) {
parsed.coolantPatch = { ...(parsed.coolantPatch || {}), mist: true };
} else if (value === 8) {
parsed.coolantPatch = { ...(parsed.coolantPatch || {}), flood: true };
} else if (value === 9) {
parsed.coolantPatch = { flood: false, mist: false };
} else if (value === 428) {
parsed.kinsType = "tcp";
} else if (value === 429) {
parsed.kinsType = "identity";
} else if (value === 430) {
parsed.kinsType = "userk";
}
}
function spindleHalPinsForState(spindle = {}) {
const enabled = Boolean(spindle.enabled) && spindle.direction !== "stop";
const speedOut = enabled
? Number(spindle.rpm || 0) * (Number(spindle.override || 100) / 100)
: 0;
return {
on: enabled ? 1 : 0,
forward: spindle.direction === "forward" ? 1 : 0,
reverse: spindle.direction === "reverse" ? 1 : 0,
speedOut,
atSpeed: enabled ? 1 : 0,
};
}
function stoppedSpindleState(spindle = {}) {
return {
...spindle,
enabled: false,
direction: "stop",
halPins: spindleHalPinsForState({
...spindle,
enabled: false,
direction: "stop",
}),
};
}
function normalizeSpindleDirection(direction) {
const value = String(direction || "stop").toLowerCase();
return value === "forward" || value === "reverse" ? value : "stop";
}
function resolveMdiKinsType(state, kinsType) {
if (kinsType !== "tcp") return kinsType;
return tcpKinsTypeForProfile(state.profile);
}
function hasMdiAxisWords(parsed) {
return Object.values(parsed.axes).some((value) => Number.isFinite(value));
}
function createMdiProgramExecution(command, axisPose, motionCode) {
const motionType = motionCode === "G0" ? "STRAIGHT_TRAVERSE" : "STRAIGHT_FEED";
return {
apiName: "web-rtcp-5axis-mdi-execution",
sourceMode: "operator-mdi",
semanticBoundary: "operator_mdi_lightweight_motion_words",
resultText: `mdi_command=${command}`,
motion: hasMdiAxisWords(parseMdiCommand(command))
? [{
type: motionType,
line: 1,
statement: command,
axes: { ...axisPose },
raw: `mdi_command=${command}`,
}]
: [],
summary: {
ready: true,
programLineCount: 1,
canonicalEventCount: 1,
motionEventCount: hasMdiAxisWords(parseMdiCommand(command)) ? 1 : 0,
motionTypes: hasMdiAxisWords(parseMdiCommand(command)) ? [motionType] : [],
finalAxes: { ...axisPose },
remapRuntimeReady: false,
plannerRuntimeReady: false,
fullLinuxCncProgramExecutionReady: false,
},
};
}
function getProgramEndLine(state) {
return state.programStartLine + Math.max(state.programLines.length - 1, 0);
}
function getNextProgramLine(state, step) {
return Math.min(state.activeLine + step, getProgramEndLine(state));
}
function nextProgramPlayback(state, step) {
if (state.programExecution?.motion?.length > 0) {
const timing = state.programExecutionTiming || buildTimingForState(state, state.programExecution);
const motionIndex = Math.min(
Number(state.programExecutionMotionIndex || 0) + step,
state.programExecution.motion.length - 1,
);
const motion = state.programExecution.motion[motionIndex];
const timingSnapshot = timingAtMotionIndex(timing, motionIndex);
const kinsType = kinsTypeFromProgramMotion(state, motion) || state.kinsType;
return {
motionIndex,
activeLine: motion.line || state.activeLine,
axisPose: axisPoseFromCanonicalMotion(motion, state.axisPose),
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
timing: timingSnapshot,
complete: motionIndex >= state.programExecution.motion.length - 1,
};
}
const activeLine = getNextProgramLine(state, step);
return {
motionIndex: state.programExecutionMotionIndex || 0,
activeLine,
axisPose: buildFixtureAxisPoseForLine(state.axisPose, activeLine),
kinsType: state.kinsType,
rtcpState: state.rtcpState,
timing: {
elapsedSeconds: 0,
remainingSeconds: 0,
currentVelocity: state.feed.currentVelocity,
segmentDurationSeconds: 0,
segmentDistanceMm: 0,
},
complete: activeLine >= getProgramEndLine(state),
};
}
function nextProgramRuntimeSamplePlayback(state, step) {
const timing = state.programExecutionTiming || buildTimingForState(state, state.programExecution);
const samples = Array.isArray(timing?.samples) ? timing.samples : [];
if (samples.length > 0) {
const sampleIndex = Math.min(
Number(state.programExecutionSampleIndex || 0) + Math.max(Number(step) || 1, 1),
samples.length - 1,
);
const sample = samples[sampleIndex];
const motionIndex = clampMotionIndex(state, sample.motionIndex);
const motion = state.programExecution?.motion?.[motionIndex] || null;
const sampleWithUnits = {
...sample,
linearUnits: sample.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits,
};
const segment = timing?.segments?.[motionIndex] || null;
const kinsType = kinsTypeFromProgramMotion(state, motion) || state.kinsType;
const elapsedSeconds = Number(sample.timeSeconds) || Number(segment?.elapsedSeconds) || 0;
const currentVelocity = Number(sample.currentVelocityMmPerMin)
|| Number(sample.currentVelocity) * 60
|| Number(segment?.velocityMmPerMin)
|| 0;
const runtimeFeedback = createProgramRuntimeFeedbackFromSample({
state,
sample: sampleWithUnits,
sampleIndex,
motion,
motionIndex,
segment,
currentVelocity,
elapsedSeconds,
});
return {
motionIndex,
sampleIndex,
activeLine: sample.line || motion?.line || state.activeLine,
axisPose: axisPoseFromRuntimeSample(sampleWithUnits, motion, state.axisPose),
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
timing: {
elapsedSeconds,
remainingSeconds: Math.max((timing?.totalSeconds || 0) - elapsedSeconds, 0),
currentVelocity,
segmentDurationSeconds: Number(segment?.durationSeconds) || 0,
segmentDistanceMm: Number(segment?.linearDistanceMm) || 0,
},
runtimeFeedback,
complete: sampleIndex >= samples.length - 1,
};
}
const pathSamples = state.programAxisPreviewPath?.samples;
if (Array.isArray(pathSamples) && pathSamples.length > 0) {
const sampleIndex = Math.min(
Number(state.programExecutionSampleIndex || 0) + Math.max(Number(step) || 1, 1),
pathSamples.length - 1,
);
const sample = pathSamples[sampleIndex];
const motionIndex = Number.isFinite(Number(sample?.segmentIndex)) ? Number(sample.segmentIndex) : 0;
const axisPose = axisPoseFromProgramPathSample(sample, state.axisPose, state.profile);
const kinsType = sample?.activeKinematics
? normalizeSampleKinsType(state, sample.activeKinematics)
: state.kinsType;
const elapsedSeconds = Number(sample?.timeMs || 0) / 1000;
const currentVelocity = Number(sample?.machineState?.feed?.actualMmPerMin || sample?.feed || 0);
const runtimeFeedback = {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "web-axis-source-execution-expanded-ngcgui-subroutines",
semanticBoundary: "linuxcnc_xyzbc_switchkins_sample_stream_feedback",
sampleIndex,
motionIndex,
sourceFile: sample?.sourceFile || null,
statement: sample?.statement || "",
line: sample?.line || state.activeLine,
type: sample?.motionType || null,
motionType: sample?.motionType || null,
linearUnits: state.profile?.traj?.linearUnits || "mm",
timeSeconds: elapsedSeconds,
axisPose,
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: currentVelocity,
distanceToGo: sampleIndex >= pathSamples.length - 1 ? 0 : 1,
dtg: { x: 0, y: 0, z: 0 },
queueDepth: 0,
activeDepth: sampleIndex >= pathSamples.length - 1 ? 0 : 1,
cycle: sampleIndex,
};
return {
motionIndex,
sampleIndex,
activeLine: sample?.line || state.activeLine,
axisPose,
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
timing: {
elapsedSeconds,
remainingSeconds: Math.max(((pathSamples.length - 1) * Number(state.programAxisPreviewPath?.samplePeriodMs || 0)) / 1000 - elapsedSeconds, 0),
currentVelocity,
segmentDurationSeconds: Number(state.programAxisPreviewPath?.samplePeriodMs || 0) / 1000,
segmentDistanceMm: 0,
},
runtimeFeedback,
complete: sampleIndex >= pathSamples.length - 1,
};
}
const playback = nextProgramPlayback(state, step);
return {
...playback,
sampleIndex: playback.motionIndex,
runtimeFeedback: createProgramRuntimeFeedbackFromMotion({
state,
motion: state.programExecution?.motion?.[playback.motionIndex] || null,
motionIndex: playback.motionIndex,
timing: playback.timing,
sourceMode: state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm"
? "linuxcnc-canonical-motion"
: "fixture-line-playback",
}),
};
}
function createInitialProgramRuntimeFeedback({ state, timing, motion, timingSnapshot }) {
const firstSample = timing?.samples?.[0] || null;
if (firstSample) {
const sampleWithUnits = {
...firstSample,
linearUnits: firstSample.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits,
};
return createProgramRuntimeFeedbackFromSample({
state,
sample: sampleWithUnits,
sampleIndex: 0,
motion,
motionIndex: clampMotionIndex(state, firstSample.motionIndex),
segment: timing?.segments?.[0] || null,
currentVelocity: Number(firstSample.currentVelocityMmPerMin)
|| Number(firstSample.currentVelocity) * 60
|| 0,
elapsedSeconds: Number(firstSample.timeSeconds) || 0,
});
}
return createProgramRuntimeFeedbackFromMotion({
state,
motion,
motionIndex: 0,
timing: timingSnapshot,
sourceMode: "linuxcnc-canonical-motion",
});
}
function clampMotionIndex(state, motionIndex) {
const count = state.programExecution?.motion?.length || 0;
if (count <= 0) return 0;
const index = Number(motionIndex);
return Number.isFinite(index) ? Math.min(Math.max(index, 0), count - 1) : 0;
}
function buildTimingForState(state, execution) {
const motion = execution?.motion || [];
const requiresFeedModeTiming = motion.some((event) => event?.feedMode === "inverse-time");
if (!requiresFeedModeTiming && execution?.plannerTiming?.plannerRuntimeReady === true) {
return execution.plannerTiming;
}
return buildProgramExecutionTiming({
motion,
profile: state.profile,
feedOverride: state.feed.feedOverride,
rapidOverride: state.feed.rapidOverride,
defaultFeedRate: state.feed.feedRate,
});
}
function kinsTypeFromProgramMotion(state, motion) {
if (!motion) return null;
if (motion.kinsType) {
return resolveProgramKinsType(state, motion.kinsType);
}
if (Number.isFinite(motion.switchkinsType)) {
return kinsTypeFromSwitchkinsType(state, motion.switchkinsType);
}
return null;
}
function resolveProgramKinsType(state, requestedKinsType) {
if (requestedKinsType === "tcp") {
return kinsTypeFromSwitchkinsType(state, 1);
}
if (requestedKinsType === "identity") {
return "identity";
}
if (requestedKinsType === "userk") {
return kinsTypeFromSwitchkinsType(state, 2);
}
return requestedKinsType || null;
}
function kinsTypeFromSwitchkinsType(state, switchkinsType) {
return state.profile.kinematicsParameters.switchkinsTypes
.find((type) => type.value === switchkinsType)?.webKinsType || null;
}
function switchkinsTypeFromKinsType(state) {
const match = state.profile.kinematicsParameters.switchkinsTypes
.find((type) => type.webKinsType === state.kinsType);
return Number.isFinite(match?.value) ? match.value : 0;
}
function rtcpStateFromKinsType(kinsType) {
return String(kinsType || "").startsWith("tcp-") ? "on" : "off";
}
function switchKinematicsRuntimeForState(state) {
if (!state.kinematicsRuntime?.loaded || typeof state.kinematicsRuntime.switchKinematics !== "function") {
return null;
}
const switchkinsType = switchkinsTypeFromKinsType(state);
if (state.kinematicsRuntime.switchkinsType === switchkinsType) {
return state.kinematicsRuntime.switchRc ?? 0;
}
return state.kinematicsRuntime.switchKinematics(switchkinsType);
}
function buildLoadedProgram(action) {
const content = String(action.content || "");
const lines = parseProgramLines(content);
const filename = action.filename || "operator-program.ngc";
return {
activeProgram: filename,
programSource: action.programSource || "operator-file",
programSourceRel: action.sourceRel || null,
programWasmPath: action.wasmPath || null,
programStartLine: 1,
activeLine: 1,
lineCount: lines.length,
fileSizeBytes: content.length,
programLines: lines,
};
}
function currentPathTool(state) {
const pathTool = state.toolRuntimeState?.pathTool;
if (
pathTool &&
(
Number(pathTool.id) > 0 ||
Number(pathTool.pocket) > 0 ||
Number(pathTool.length) > 0 ||
Number(pathTool.diameter) > 0
)
) {
return pathTool;
}
if (state.machineProfile === "xyzbc-trt") {
return {
id: 2,
pocket: 2,
length: 10,
diameter: 8,
};
}
return {
id: Number(state.toolPreview?.toolNumber) || 2,
pocket: Number(state.toolPreview?.toolNumber) || 2,
length: Number(state.toolPreview?.length) || 10,
diameter: Number(state.toolPreview?.diameter) || 8,
};
}
function parseProgramLines(content) {
const lines = content
.split(/\r?\n/)
.map((line) => line.trimEnd())
.filter((line) => line.trim().length > 0);
return lines.length > 0 ? lines : ["(empty program)"];
}
function clampPercent(value, min, max) {
return Math.min(Math.max(value, min), max);
}
const GMOCAPY_OVERRIDE_TARGETS = {
feed: {
countPin: "gmoccapy.feed.feed-override.counts",
countEnablePin: "gmoccapy.feed.feed-override.count-enable",
analogEnablePin: "gmoccapy.feed.feed-override.analog-enable",
directValuePin: "gmoccapy.feed.feed-override.direct-value",
resetPin: "gmoccapy.feed.reset-feed-override",
countStateKey: "feedOverrideCounts",
countEnableStateKey: "feedOverrideCountEnabled",
analogEnableStateKey: "feedOverrideAnalogEnabled",
stateDomain: "feed",
stateKey: "feedOverride",
min: 0,
max: 200,
scale: 1,
},
rapid: {
countPin: "gmoccapy.rapid.rapid-override.counts",
countEnablePin: "gmoccapy.rapid.rapid-override.count-enable",
analogEnablePin: "gmoccapy.rapid.rapid-override.analog-enable",
directValuePin: "gmoccapy.rapid.rapid-override.direct-value",
resetPin: "gmoccapy.rapid.reset-rapid-override",
countStateKey: "rapidOverrideCounts",
countEnableStateKey: "rapidOverrideCountEnabled",
analogEnableStateKey: "rapidOverrideAnalogEnabled",
stateDomain: "feed",
stateKey: "rapidOverride",
min: 0,
max: 200,
scale: 1,
},
spindle: {
countPin: "gmoccapy.spindle.spindle-override.counts",
countEnablePin: "gmoccapy.spindle.spindle-override.count-enable",
analogEnablePin: "gmoccapy.spindle.spindle-override.analog-enable",
directValuePin: "gmoccapy.spindle.spindle-override.direct-value",
resetPin: "gmoccapy.spindle.reset-spindle-override",
countStateKey: "spindleOverrideCounts",
countEnableStateKey: "spindleOverrideCountEnabled",
analogEnableStateKey: "spindleOverrideAnalogEnabled",
stateDomain: "spindle",
stateKey: "override",
min: 0,
max: 150,
scale: 1,
},
jogVelocity: {
countPin: "gmoccapy.jog.jog-velocity.counts",
countEnablePin: "gmoccapy.jog.jog-velocity.count-enable",
analogEnablePin: "gmoccapy.jog.jog-velocity.analog-enable",
directValuePin: "gmoccapy.jog.jog-velocity.direct-value",
countStateKey: "jogVelocityCounts",
countEnableStateKey: "jogVelocityCountEnabled",
analogEnableStateKey: "jogVelocityAnalogEnabled",
stateDomain: "gmoccapyGui",
stateKey: "jogVelocity",
min: 0,
max: 14040,
scale: 140.4,
},
};
function applyGmoccapyHalPinPatch(state, action, model = gmoccapyHalModel) {
const pin = normalizeGmoccapyHalPin(action.pin);
const value = action.value;
if (!pin) {
return { applied: false, operatorMessage: "gmoccapy HAL pin blocked: missing pin" };
}
if (!isKnownGmoccapyHalPin(pin, model)) {
return { applied: false, operatorMessage: `gmoccapy HAL pin unmapped: ${pin}` };
}
if (pin === "gmoccapy.ignore-limits") {
const enabled = Boolean(value);
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
ignoreLimits: enabled,
lastHalPinEffect: enabled
? "chk_ignore_limits set active; command.override_limits() requested"
: "chk_ignore_limits cleared",
}),
operatorMessage: enabled ? "gmoccapy HAL ignore-limits requested" : "gmoccapy HAL ignore-limits cleared",
},
};
}
if (pin === "gmoccapy.optional-stop") {
const enabled = Boolean(value);
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
optionalBlocks: enabled,
lastHalPinEffect: "optional-stop pin drives tbtn_optional_blocks -> set_block_delete",
}),
operatorMessage: `gmoccapy HAL optional-stop -> block delete ${enabled ? "on" : "off"}`,
},
};
}
if (pin === "gmoccapy.blockdelete") {
const enabled = Boolean(value);
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
optionalStop: enabled,
lastHalPinEffect: "blockdelete pin drives command.set_optional_stop",
}),
operatorMessage: `gmoccapy HAL blockdelete -> optional stop ${enabled ? "on" : "off"}`,
},
};
}
if (pin === "gmoccapy.unlock-settings") {
const enabled = Boolean(value);
const halUnlockActive = action.halUnlockMode === true || state.gmoccapyGui?.settingsUnlockMode === "hal";
if (!halUnlockActive) {
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
settingsUnlockPin: enabled,
setupSensitive: true,
lastHalPinEffect: "unlock-settings ignored because unlock_way is not hal",
}),
operatorMessage: "gmoccapy HAL unlock-settings ignored: unlock_way is use",
},
};
}
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
settingsUnlockMode: "hal",
settingsUnlockPin: enabled,
setupSensitive: enabled,
lastHalPinEffect: `unlock-settings ${enabled ? "enabled" : "disabled"} setup page sensitivity`,
}),
operatorMessage: `gmoccapy HAL unlock-settings ${enabled ? "enabled setup" : "disabled setup"}`,
},
};
}
const jogAxisPin = resolveGmoccapyJogAxisPin(pin, model);
if (jogAxisPin) {
return applyGmoccapyJogAxisHalPatch(state, pin, value, jogAxisPin);
}
const jogIncrement = resolveGmoccapyJogIncrementPin(pin, model);
if (jogIncrement) {
return applyGmoccapyJogIncrementHalPatch(state, pin, value, jogIncrement);
}
if (pin === "gmoccapy.jog.turtle-jog") {
const enabled = Boolean(value);
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
turtleJog: enabled,
lastHalPinEffect: `turtle jog ${enabled ? "enabled" : "disabled"} by level-driven pin`,
}),
operatorMessage: `gmoccapy HAL turtle jog ${enabled ? "on" : "off"}`,
},
};
}
if (pin === "gmoccapy.delete-message") {
if (!value) {
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
lastHalPinEffect: "delete-message falling edge ignored",
}),
operatorMessage: "gmoccapy HAL delete-message falling edge ignored",
},
};
}
const hadError = state.gmoccapyGui?.error === true || /error|blocked|warning/i.test(state.operatorMessage || "");
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
error: false,
deletedMessageCount: Number(state.gmoccapyGui?.deletedMessageCount || 0) + 1,
lastHalPinEffect: hadError
? "delete-message removed first alert and cleared gmoccapy.error"
: "delete-message deleted last notification",
}),
operatorMessage: hadError
? "gmoccapy HAL delete-message cleared alert"
: "gmoccapy HAL delete-message deleted last message",
},
};
}
if (pin === "gmoccapy.warning-confirm") {
const enabled = Boolean(value);
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
warningConfirm: enabled,
lastHalPinEffect: enabled
? "warning-confirm level would accept active warning dialog"
: "warning-confirm level cleared",
}),
operatorMessage: enabled
? "gmoccapy HAL warning-confirm asserted"
: "gmoccapy HAL warning-confirm cleared",
},
};
}
const toolMeasurementPin = resolveGmoccapyToolMeasurementPin(pin, model);
if (toolMeasurementPin) {
return applyGmoccapyToolMeasurementHalPatch(state, pin, value, toolMeasurementPin);
}
if (pin.startsWith("gmoccapy.messages.")) {
return {
applied: false,
operatorMessage: "gmoccapy HAL user message pin unmapped: gmoccapy_XYZAB.ini defines no MESSAGE_* entries",
};
}
const overrideTarget = findOverrideTargetForPin(pin);
if (overrideTarget) {
return applyGmoccapyOverrideHalPatch(state, pin, value, overrideTarget);
}
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
lastHalPinEffect: "diagnostic-only gmoccapy HAL pin",
}),
operatorMessage: `gmoccapy HAL diagnostic-only: ${pin}`,
},
};
}
function normalizeGmoccapyHalPin(pin) {
const value = String(pin || "").trim();
if (!value) return "";
return value.startsWith("gmoccapy.") ? value : `gmoccapy.${value}`;
}
function isKnownGmoccapyHalPin(pin, model = gmoccapyHalModel) {
if (model.nativePins.some((group) => group.pins.includes(pin))) return true;
return pin.startsWith("gmoccapy.messages.");
}
function halGuiPatch(state, pin, value, patch = {}) {
return {
...state.gmoccapyGui,
...patch,
lastHalPin: pin,
lastHalPinValue: value,
};
}
function applyGmoccapyNativePagePatch(state, action, model = gmoccapyHalModel) {
const pageId = String(action.pageId || action.page || "").trim();
const page = model.nativePages?.implementationMatrix?.find((entry) => entry.pageId === pageId);
if (!page) {
return {
operatorMessage: `gmoccapy native page unmapped: ${pageId || "unknown"}`,
};
}
return {
gmoccapyGui: {
...state.gmoccapyGui,
activeNativePage: page.pageId,
nativePageMode: page.implementation,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: `${page.nativeWidget} ${page.implementation}`,
},
operatorMessage: page.implementation === "diagnostic-only" || page.implementation === "native-only"
? `gmoccapy native page diagnostic-only: ${page.pageId}`
: `gmoccapy native page ${page.pageId}`,
};
}
function applyGmoccapyPageActionPatch(state, action, model = gmoccapyHalModel) {
const pageId = String(action.pageId || action.page || "").trim();
const actionId = String(action.actionId || action.action || "open").trim();
if (pageId === "file-load") {
const running = state.machine?.interpState === "reading" || state.runState === "running";
const status = running ? "blocked-running" : actionId;
return {
gmoccapyGui: {
...state.gmoccapyGui,
activeNativePage: "file-load",
nativePageMode: model.nativePages.filePage.implementation,
filePageStatus: status,
filePageLastAction: actionId,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: running
? "file load blocked while interpreter is running"
: "IconFileSelection native page represented by Web file/staged-source controls",
},
operatorMessage: running
? "gmoccapy file page blocked: interpreter running"
: `gmoccapy file page ${actionId}: native Gtk chooser diagnostic`,
};
}
return {
operatorMessage: `gmoccapy page action unmapped: ${pageId || "unknown"}`,
};
}
function applyGmoccapyMacroPatch(state, action, model = gmoccapyHalModel) {
const macroName = String(action.name || action.macro || "").trim();
const macro = model.nativePages?.macroPage?.macros?.find((entry) => entry.name === macroName);
if (!macro) {
return {
applied: false,
operatorMessage: `gmoccapy macro unmapped: ${macroName || "unknown"}`,
};
}
const gate = gateLinuxCncTaskAction(state, { type: "RUN_MDI" });
if (!gate.allowed) {
return {
applied: true,
patch: {
gmoccapyGui: {
...state.gmoccapyGui,
macroButtonsEnabled: false,
macroLastName: macro.name,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: `macro ${macro.name} blocked: ${gate.operatorMessage}`,
},
operatorMessage: `gmoccapy macro blocked: ${gate.operatorMessage}`,
},
};
}
const command = buildGmoccapyMacroCommand(macro, action.args || action.parameters || {});
const mdiResult = executeMdiCommand(state, command);
return {
applied: true,
patch: {
...mdiResult.patch,
gmoccapyGui: {
...state.gmoccapyGui,
activeNativePage: "mdi-macros",
nativePageMode: "partial",
macroButtonsEnabled: false,
macroLastName: macro.name,
macroLastCommand: command,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: `macro ${macro.name} dispatched as MDI O-word call`,
},
operatorMessage: `gmoccapy macro MDI ${command}`,
},
};
}
function buildGmoccapyMacroCommand(macro, args = {}) {
const values = Array.isArray(args)
? args
: macro.args.map((name) => args[name] ?? args[String(name).toLowerCase()] ?? 0);
return [
`O<${macro.name}> call`,
...macro.args.map((name, index) => `[${values[index] ?? 0}]`),
].join(" ");
}
function applyGmoccapyToolEditorPatch(state, action, model = gmoccapyHalModel) {
const actionId = String(action.actionId || action.action || "open").trim();
const writableAction = ["save", "add", "delete", "touch-off"].includes(actionId);
if (writableAction && model.nativePages?.toolEditorPage?.editableInWeb === false) {
return {
gmoccapyGui: {
...state.gmoccapyGui,
activeNativePage: "tool-editor",
nativePageMode: "diagnostic-only",
toolEditorStatus: "writeback-blocked",
toolEditorLastAction: actionId,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: "tool editor writeback disabled in browser",
},
operatorMessage: `gmoccapy tool editor diagnostic-only: ${actionId} does not write tool.tbl`,
};
}
return {
gmoccapyGui: {
...state.gmoccapyGui,
activeNativePage: "tool-editor",
nativePageMode: "diagnostic-only",
toolEditorStatus: actionId,
toolEditorLastAction: actionId,
lastHalPin: null,
lastHalPinValue: null,
lastHalPinEffect: "tooledit1 native page represented by diagnostics",
},
operatorMessage: `gmoccapy tool editor ${actionId}: ${state.profile?.toolTable?.toolCount || 0} tools diagnostic-only`,
};
}
function findOverrideTargetForPin(pin) {
return Object.entries(GMOCAPY_OVERRIDE_TARGETS)
.find(([, target]) => [
target.countPin,
target.countEnablePin,
target.analogEnablePin,
target.directValuePin,
target.resetPin,
].includes(pin)) || null;
}
function resolveGmoccapyToolMeasurementPin(pin, model = gmoccapyHalModel) {
return model.halPinActions?.toolMeasurementPins?.pins?.find((entry) => entry.pin === pin) || null;
}
function resolveGmoccapyJogAxisPin(pin, model = gmoccapyHalModel) {
for (const entry of model.halPinActions?.jogPins?.axes || []) {
if (pin === entry.plus) return { axis: entry.axis, direction: 1 };
if (pin === entry.minus) return { axis: entry.axis, direction: -1 };
}
return null;
}
function resolveGmoccapyJogIncrementPin(pin, model = gmoccapyHalModel) {
return (model.halPinActions?.jogPins?.increments || []).find((entry) => entry.pin === pin) || null;
}
function applyGmoccapyJogAxisHalPatch(state, pin, value, jogAxisPin) {
const axis = String(jogAxisPin.axis || "").toLowerCase();
const direction = Number(jogAxisPin.direction || 1);
const axisLabel = axis.toUpperCase();
const sign = direction > 0 ? "+" : "-";
if (!value) {
const isActivePin = state.gmoccapyGui?.activeJogPin === pin;
return {
applied: true,
patch: {
runState: isActivePin && state.runState === "jogging" ? "idle" : state.runState,
gmoccapyGui: halGuiPatch(state, pin, value, {
activeJogPin: isActivePin ? null : state.gmoccapyGui?.activeJogPin || null,
lastHalPinEffect: `jog ${axisLabel}${sign} released`,
}),
operatorMessage: `gmoccapy HAL jog ${axisLabel}${sign} released`,
},
};
}
const gate = gateLinuxCncTaskAction(state, { type: "JOG", axis, direction });
if (!gate.allowed) {
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
lastHalPinEffect: `jog ${axisLabel}${sign} ignored: ${gate.operatorMessage}`,
}),
operatorMessage: `gmoccapy HAL jog blocked: ${gate.operatorMessage}`,
},
};
}
const increment = Number(state.gmoccapyGui?.jogIncrementOutput ?? state.machine?.jogIncrement ?? 0);
const continuous = Math.abs(increment) <= 0;
const nextAxisPose = continuous
? state.axisPose
: {
...state.axisPose,
[axis]: Number(state.axisPose?.[axis] || 0) + direction * increment,
};
return {
applied: true,
patch: {
machine: {
...state.machine,
mode: "manual",
jogAxis: axis,
jogIncrement: increment,
},
axisPose: nextAxisPose,
runState: "jogging",
gmoccapyGui: halGuiPatch(state, pin, value, {
activeJogPin: pin,
lastHalPinEffect: continuous
? `jog ${axisLabel}${sign} continuous press`
: `jog ${axisLabel}${sign} incremental ${increment}`,
}),
operatorMessage: continuous
? `gmoccapy HAL jog ${axisLabel}${sign} continuous`
: `gmoccapy HAL jog ${axisLabel}${sign} ${increment}`,
},
};
}
function applyGmoccapyJogIncrementHalPatch(state, pin, value, jogIncrement) {
if (!value) {
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
lastHalPinEffect: `jog increment ${jogIncrement.index} falling edge ignored`,
}),
operatorMessage: "gmoccapy HAL jog increment falling edge ignored",
},
};
}
const distance = Number(jogIncrement.distance || 0);
const label = String(jogIncrement.label || `increment ${jogIncrement.index}`);
return {
applied: true,
patch: {
machine: {
...state.machine,
jogIncrement: distance,
},
runState: state.machine.mode === "manual" && state.runState === "jogging" ? "idle" : state.runState,
gmoccapyGui: halGuiPatch(state, pin, value, {
jogIncrementIndex: Number(jogIncrement.index),
jogIncrementLabel: label,
jogIncrementOutput: distance,
activeJogPin: null,
lastHalPinEffect: `jog increment selected ${label}; gmoccapy.jog.jog-increment=${distance}`,
}),
operatorMessage: `gmoccapy HAL jog increment ${label}`,
},
};
}
function applyGmoccapyToolMeasurementHalPatch(state, pin, value, toolMeasurementPin) {
const nextValue = toolMeasurementPin.pin === "gmoccapy.toolmeasurement"
? Boolean(value)
: Number(value || 0);
const patch = {
lastHalPinEffect: "tool measurement HAL OUT pin recorded as diagnostic-only in Web",
};
if (toolMeasurementPin.pin === "gmoccapy.probeheight") patch.probeHeight = nextValue;
if (toolMeasurementPin.pin === "gmoccapy.blockheight") patch.blockHeight = nextValue;
if (toolMeasurementPin.pin === "gmoccapy.toolmeasurement") patch.toolMeasurement = nextValue;
if (toolMeasurementPin.pin === "gmoccapy.searchvel") patch.searchVelocity = nextValue;
if (toolMeasurementPin.pin === "gmoccapy.probevel") patch.probeVelocity = nextValue;
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, patch),
operatorMessage: "gmoccapy HAL tool measurement output recorded; XYZAB has no [TOOLSENSOR]",
},
};
}
function applyGmoccapyOverrideHalPatch(state, pin, value, [targetName, target]) {
if (pin === target.countEnablePin || pin === target.analogEnablePin) {
const enabled = Boolean(value);
const key = pin === target.countEnablePin ? target.countEnableStateKey : target.analogEnableStateKey;
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
[key]: enabled,
lastHalPinEffect: `${targetName} ${pin === target.countEnablePin ? "count" : "analog"} input ${enabled ? "enabled" : "disabled"}`,
}),
operatorMessage: `gmoccapy HAL ${targetName} ${pin === target.countEnablePin ? "counts" : "analog"} ${enabled ? "enabled" : "disabled"}`,
},
};
}
if (pin === target.resetPin) {
if (!value) {
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
lastHalPinEffect: `${targetName} reset falling edge ignored`,
}),
operatorMessage: `gmoccapy HAL ${targetName} reset falling edge ignored`,
},
};
}
return {
applied: true,
patch: resetOverridePatchForTarget(state, targetName, { pin, value }),
};
}
if (pin === target.directValuePin) {
if (!state.gmoccapyGui?.[target.analogEnableStateKey]) {
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
lastHalPinEffect: `${targetName} direct-value ignored until analog-enable is true`,
}),
operatorMessage: `gmoccapy HAL ${targetName} direct-value ignored: analog disabled`,
},
};
}
const normalized = clampNumber(Number(value), 0, 1);
const nextValue = target.min + (target.max - target.min) * normalized;
return overrideValuePatch(state, targetName, target, nextValue, {
pin,
value,
effect: `${targetName} direct-value ${normalized}`,
operatorMessage: `gmoccapy HAL ${targetName} direct-value ${Math.round(nextValue)}`,
});
}
if (pin === target.countPin) {
const counts = Number(value);
if (!Number.isFinite(counts)) {
return { applied: false, operatorMessage: `gmoccapy HAL ${targetName} counts invalid: ${value}` };
}
if (!state.gmoccapyGui?.[target.countEnableStateKey]) {
return {
applied: true,
patch: {
gmoccapyGui: halGuiPatch(state, pin, value, {
[target.countStateKey]: counts,
lastHalPinEffect: `${targetName} counts synchronized while count-enable is false`,
}),
operatorMessage: `gmoccapy HAL ${targetName} counts synchronized`,
},
};
}
const previousCounts = Number(state.gmoccapyGui?.[target.countStateKey] || 0);
const delta = (counts - previousCounts) * target.scale;
const currentValue = currentOverrideValue(state, target);
return overrideValuePatch(state, targetName, target, currentValue + delta, {
pin,
value,
countPatch: { [target.countStateKey]: counts },
effect: `${targetName} counts delta ${delta}`,
operatorMessage: `gmoccapy HAL ${targetName} counts adjusted`,
});
}
return { applied: false, operatorMessage: `gmoccapy HAL override pin unmapped: ${pin}` };
}
function overrideValuePatch(state, targetName, target, rawValue, {
pin,
value,
countPatch = {},
effect,
operatorMessage,
} = {}) {
const nextValue = clampPercent(rawValue, target.min, target.max);
const gmoccapyGui = halGuiPatch(state, pin, value, {
...countPatch,
lastHalPinEffect: effect,
});
if (target.stateDomain === "feed") {
return {
applied: true,
patch: {
feed: {
...state.feed,
[target.stateKey]: Math.round(nextValue),
},
gmoccapyGui,
operatorMessage,
},
};
}
if (target.stateDomain === "spindle") {
return {
applied: true,
patch: {
spindle: {
...state.spindle,
[target.stateKey]: Math.round(nextValue),
},
gmoccapyGui,
operatorMessage,
},
};
}
return {
applied: true,
patch: {
gmoccapyGui: {
...gmoccapyGui,
[target.stateKey]: Number(nextValue.toFixed(3)),
},
operatorMessage,
},
};
}
function currentOverrideValue(state, target) {
if (target.stateDomain === "feed") return Number(state.feed?.[target.stateKey] || 0);
if (target.stateDomain === "spindle") return Number(state.spindle?.[target.stateKey] || 0);
return Number(state.gmoccapyGui?.[target.stateKey] || 0);
}
function resetOverridePatchForTarget(state, targetName, hal = {}) {
const target = GMOCAPY_OVERRIDE_TARGETS[targetName];
if (!target || targetName === "jogVelocity") {
return {
gmoccapyGui: halGuiPatch(state, hal.pin || null, hal.value ?? null, {
lastHalPinEffect: `reset unsupported for ${targetName}`,
}),
operatorMessage: `override reset unsupported: ${targetName}`,
};
}
const effect = `${targetName} override reset to 100`;
const gmoccapyGui = halGuiPatch(state, hal.pin || null, hal.value ?? null, {
lastHalPinEffect: hal.pin ? `HAL ${effect}` : effect,
});
if (target.stateDomain === "spindle") {
return {
spindle: {
...state.spindle,
[target.stateKey]: 100,
},
gmoccapyGui,
operatorMessage: hal.pin ? `gmoccapy HAL ${targetName} reset to 100` : `${targetName} override reset`,
};
}
return {
feed: {
...state.feed,
[target.stateKey]: 100,
},
gmoccapyGui,
operatorMessage: hal.pin ? `gmoccapy HAL ${targetName} reset to 100` : `${targetName} override reset`,
};
}
function clampNumber(value, min, max) {
if (!Number.isFinite(value)) return min;
return Math.min(Math.max(value, min), max);
}
function buildDroFromFrame(frame, runtimeFeedback = null) {
const dtg = runtimeFeedback?.dtg || null;
return {
x: frame.axisPose.x,
y: frame.axisPose.y,
z: frame.axisPose.z,
a: frame.axisPose.a,
b: frame.axisPose.b,
c: frame.axisPose.c,
tcpX: frame.tcpPose.x,
tcpY: frame.tcpPose.y,
tcpZ: frame.tcpPose.z,
dtgX: Number.isFinite(dtg?.x) ? dtg.x : frame.rtcpEnabled ? Math.abs(frame.compensation.x) : 0,
dtgY: Number.isFinite(dtg?.y) ? dtg.y : frame.rtcpEnabled ? Math.abs(frame.compensation.y) : 0.01,
dtgZ: Number.isFinite(dtg?.z) ? dtg.z : frame.rtcpEnabled ? Math.abs(frame.compensation.z) : 2.25,
};
}
function jointsFromAxisPose(axisPose, profile = defaultProfile) {
const fourthAxis = profile.traj?.coordinates?.includes("B") ? "b" : "a";
return [
Number(axisPose.x || 0),
Number(axisPose.y || 0),
Number(axisPose.z || 0),
Number(axisPose[fourthAxis] || 0),
Number(axisPose.c || 0),
];
}
function buildFixtureAxisPoseForLine(axisPose, line) {
const phase = (line - 496) * 0.17;
return {
...axisPose,
x: 43 + Math.sin(phase) * 4,
y: -32.15 + Math.cos(phase) * 2.5,
z: -11.306 + Math.sin(phase * 0.7) * 1.2,
a: Math.sin(phase * 0.45) * 18,
c: Math.cos(phase * 0.33) * 32,
};
}
function axisPoseFromCanonicalMotion(motion, fallbackPose) {
const axes = motion?.axes || {};
return {
x: Number(axes.x ?? fallbackPose.x ?? 0),
y: Number(axes.y ?? fallbackPose.y ?? 0),
z: Number(axes.z ?? fallbackPose.z ?? 0),
a: Number(axes.a ?? fallbackPose.a ?? 0),
b: Number(axes.b ?? fallbackPose.b ?? 0),
c: Number(axes.c ?? fallbackPose.c ?? 0),
};
}
function axisPoseFromRuntimeSample(sample, motion, fallbackPose) {
const sampleAxes = sample?.axes || {};
const canonicalAxes = motion?.axes || {};
return {
x: numberOrFallback(sampleAxes.x, canonicalAxes.x, fallbackPose.x, 0),
y: numberOrFallback(sampleAxes.y, canonicalAxes.y, fallbackPose.y, 0),
z: numberOrFallback(sampleAxes.z, canonicalAxes.z, fallbackPose.z, 0),
a: numberOrFallback(sampleAxes.a, canonicalAxes.a, fallbackPose.a, 0),
b: numberOrFallback(sampleAxes.b, canonicalAxes.b, fallbackPose.b, 0),
c: numberOrFallback(sampleAxes.c, canonicalAxes.c, fallbackPose.c, 0),
};
}
function createProgramRuntimeFeedbackFromSample({
state,
sample,
sampleIndex,
motion,
motionIndex,
segment,
currentVelocity,
elapsedSeconds,
}) {
const axisPose = axisPoseFromRuntimeSample(sample, motion, state.axisPose);
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "linuxcnc-tp-runtime-sample",
semanticBoundary: "linuxcnc_tp_run_cycle_feedback_without_hardware",
sampleIndex,
motionIndex,
line: sample?.line || motion?.line || null,
type: sample?.type || motion?.type || null,
linearUnits: sample?.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits || "mm",
timeSeconds: elapsedSeconds,
axisPose,
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: Number(sample?.requestedVelocityMmPerMin)
|| Number(sample?.requestedVelocity) * 60
|| Number(segment?.velocityMmPerMin)
|| 0,
distanceToGo: Number(sample?.distanceToGo) || 0,
dtg: {
x: Number(sample?.dtg?.x) || 0,
y: Number(sample?.dtg?.y) || 0,
z: Number(sample?.dtg?.z) || 0,
},
queueDepth: Number(sample?.queueDepth) || 0,
activeDepth: Number(sample?.activeDepth) || 0,
cycle: Number(sample?.cycle) || 0,
};
}
function createProgramRuntimeFeedbackFromMotion({
state,
motion,
motionIndex,
timing,
sourceMode,
}) {
const axisPose = axisPoseFromCanonicalMotion(motion, state.axisPose);
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode,
semanticBoundary: sourceMode === "fixture-line-playback"
? "fixture_line_playback_feedback"
: "linuxcnc_canonical_motion_feedback_without_tp_sample",
sampleIndex: motionIndex,
motionIndex,
line: motion?.line || null,
type: motion?.type || null,
linearUnits: motion?.linearUnits || state.profile.traj?.linearUnits || "mm",
timeSeconds: Number(timing?.elapsedSeconds) || 0,
axisPose,
currentVelocityMmPerMin: Number(timing?.currentVelocity) || 0,
requestedVelocityMmPerMin: Number(timing?.currentVelocity) || 0,
distanceToGo: 0,
dtg: { x: 0, y: 0, z: 0 },
queueDepth: 0,
activeDepth: 0,
cycle: 0,
};
}
function numberOrFallback(...values) {
for (const value of values) {
const number = Number(value);
if (Number.isFinite(number)) return number;
}
return 0;
}