6431 lines
234 KiB
JavaScript
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;
|
|
}
|