Validate AXIS task state flow

This commit is contained in:
wangdequan
2026-07-09 18:12:01 -04:00
parent f99ba2bbe8
commit f4b9911d45
57 changed files with 195248 additions and 647 deletions

View File

@@ -212,15 +212,22 @@ export function buildTaskHalProgramMotionPlan({
}
export function normalizeTaskHalStatus(status = {}) {
const motion = status.motionStatus?.motion || {};
const axis = status.motionStatus?.axis || {};
rejectLegacyTaskHalStatusFields(status);
const emcStatus = requireStandaloneEmcStatus(status);
const emcTask = emcStatus.task;
const emcMotion = emcStatus.motion;
const emcTraj = emcMotion.traj || {};
const task = normalizeStatusTask(emcTask);
const axis = normalizeStatusAxis(emcMotion);
const motion = normalizeStatusMotion(emcMotion, emcTraj, task);
const commandQueueDepth = firstDefined(emcMotion.commandQueueDepth, emcTraj.queue, 0);
const statusCycle = firstDefined(emcMotion.cycle, emcTraj.cycle, Number(task.cycle || 0) * 10, 0);
const halPins = status.halSnapshot?.pins || {};
const motionProgramLine = Number(motion.programLine || 0);
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
const motionQueueDepth = Number(
status.motionStatus?.commandQueueDepth ??
commandQueueDepth ??
motion.commandQueueDepth ??
status.motionStatus?.queueDepth ??
motion.queueDepth ??
0,
);
@@ -229,45 +236,49 @@ export function normalizeTaskHalStatus(status = {}) {
: halProgramLine > 0
? halProgramLine
: 1;
const taskRuntimeReady = true;
const taskMotionReady = emcMotion.valid === true;
const halProgramLineReady = Boolean(halPins["motion.program-line"]);
const halRuntimeReady = Boolean(status.halSnapshot?.halRuntimeReady ?? status.halSnapshot?.ready ?? true);
return {
...status,
semanticBoundary: SEMANTIC_BOUNDARY,
summary: {
taskRuntimeReady: status.taskRuntimeReady === true,
motionRuntimeReady: status.motionStatus?.motionHalSyncReady === true || status.taskCommandsDriveMotionRuntime === true,
halRuntimeReady: Boolean(status.halSnapshot?.halRuntimeReady ?? status.halSnapshot?.ready ?? true),
halSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
taskHalComparisonReady: status.taskRuntimeReady === true && status.taskCommandsDriveMotionRuntime === true,
taskRuntimeReady,
motionRuntimeReady: taskMotionReady,
halRuntimeReady,
halSyncReady: taskMotionReady && halProgramLineReady,
taskHalComparisonReady: taskRuntimeReady && taskMotionReady,
switchkinsRemapHalSync: Boolean(halPins["motion.switchkins-type"]),
nativeTaskReady: status.taskRuntimeReady === true,
nativeHalSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
nativeTaskReady: taskRuntimeReady,
nativeHalSyncReady: taskMotionReady && halProgramLineReady,
fullLinuxCncProgramExecutionReady: false,
hardwareDrive: false,
hostRealtimeKernel: false,
},
ui: {
taskState: String(status.task?.state || "ESTOP").toLowerCase(),
taskMode: String(status.task?.mode || "MANUAL").toLowerCase(),
interpState: String(status.task?.interpState || "IDLE").toLowerCase(),
interpResumeState: String(status.task?.interpResumeState || status.task?.interpState || "IDLE").toLowerCase(),
execState: String(status.task?.execState || "DONE").toLowerCase(),
taskPaused: status.task?.taskPaused === true,
singleStepping: status.task?.singleStepping === true,
taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0),
programOpen: status.task?.programOpen === true,
programFile: status.task?.file || status.task?.openProgram || null,
programStartLine: Number(status.task?.programStartLine || 0),
planId: Number(status.task?.planId || 0),
motionPlanLoaded: status.task?.motionPlanLoaded === true,
errorText: status.task?.errorText || null,
allHomed: status.task?.allHomed === true,
homed: normalizeTaskHalHomed(status.task?.homed, status.task?.allHomed === true),
noForceHoming: status.task?.noForceHoming === true,
homing: status.task?.homing === true,
homeState: String(status.task?.homeState || "UNHOMED").toLowerCase(),
motionEnabled: String(status.task?.state || "").toUpperCase() === "ON" || motion.enabled === true,
resumeInhibit: status.task?.resumeInhibit === true,
servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0),
taskState: String(task.state || "ESTOP").toLowerCase(),
taskMode: String(task.mode || "MANUAL").toLowerCase(),
interpState: String(task.interpState || "IDLE").toLowerCase(),
interpResumeState: String(task.interpResumeState || task.interpState || "IDLE").toLowerCase(),
execState: String(task.execState || "DONE").toLowerCase(),
taskPaused: task.taskPaused === true,
singleStepping: task.singleStepping === true,
taskCycle: Number(task.taskCycle ?? 0),
programOpen: task.programOpen === true,
programFile: task.file || task.openProgram || null,
programStartLine: Number(task.programStartLine || 0),
planId: Number(task.planId || 0),
motionPlanLoaded: task.motionPlanLoaded === true,
errorText: task.errorText || null,
allHomed: task.allHomed === true,
homed: normalizeTaskHalHomed(task.homed, task.allHomed === true),
noForceHoming: task.noForceHoming === true,
homing: task.homing === true,
homeState: String(task.homeState || "UNHOMED").toLowerCase(),
motionEnabled: String(task.state || "").toUpperCase() === "ON" || motion.enabled === true,
resumeInhibit: task.resumeInhibit === true,
servoCycle: Number(statusCycle ?? task.servoCycle ?? 0),
motionQueueDepth,
motionPaused: motion.paused === true,
motionStepping: motion.stepping === true,
@@ -279,25 +290,92 @@ export function normalizeTaskHalStatus(status = {}) {
activeLine,
motionProgramLine,
halProgramLine,
activeLineSource: motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
activeLineSource: motionProgramLine > 0 ? "emc-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0
? motionProgramLine === halProgramLine
: false,
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
axisPose: {
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
y: Number(axis.y ?? halPins["axis.1.pos-cmd"]?.value ?? 0),
z: Number(axis.z ?? halPins["axis.2.pos-cmd"]?.value ?? 0),
a: Number(axis.a ?? halPins["axis.3.pos-cmd"]?.value ?? 0),
b: Number(axis.b ?? halPins["axis.4.pos-cmd"]?.value ?? 0),
c: Number(axis.c ?? halPins["axis.5.pos-cmd"]?.value ?? 0),
x: Number(axis.x ?? 0),
y: Number(axis.y ?? 0),
z: Number(axis.z ?? 0),
a: Number(axis.a ?? 0),
b: Number(axis.b ?? 0),
c: Number(axis.c ?? 0),
},
axisPoseFrame: isJogMotion(motion) ? "task-local" : "work",
currentVelocity: Number(motion.currentVel || motion.currentVelocity || 0) * 60,
currentVelocity: Number(motion.currentVel ?? motion.currentVelocity ?? 0) * 60,
},
};
}
function rejectLegacyTaskHalStatusFields(status = {}) {
const legacyFields = ["taskTopLevelStatus", "rcsStatus", "task", "servoCycle", "motionStatus"];
const present = legacyFields.filter((field) => Object.hasOwn(status, field));
if (present.length > 0) {
throw new Error(`Task/HAL status contains removed legacy fields: ${present.join(", ")}`);
}
}
function requireStandaloneEmcStatus(status = {}) {
const emcStatus = status.emcStatus;
if (!emcStatus || typeof emcStatus !== "object") {
throw new Error("Task/HAL status requires StandaloneEmcStatus emcStatus");
}
if (!emcStatus.task || typeof emcStatus.task !== "object") {
throw new Error("Task/HAL status requires StandaloneEmcStatus emcStatus.task");
}
if (!emcStatus.motion || typeof emcStatus.motion !== "object") {
throw new Error("Task/HAL status requires StandaloneEmcStatus emcStatus.motion");
}
return emcStatus;
}
function normalizeStatusTask(emcTask = {}) {
return {
...emcTask,
taskCycle: firstDefined(emcTask.taskCycle, emcTask.cycle, 0),
};
}
function normalizeStatusMotion(emcMotion = {}, emcTraj = {}, task = {}) {
return {
...emcMotion,
enabled: firstDefined(emcMotion.enabled, emcTraj.enabled),
inPosition: firstDefined(emcMotion.inPosition, emcTraj.inpos),
queueDepth: firstDefined(emcMotion.queueDepth, emcTraj.queue),
commandQueueDepth: firstDefined(emcMotion.commandQueueDepth, emcTraj.queue),
activeDepth: firstDefined(emcMotion.activeDepth, emcTraj.activeQueue),
queueFull: firstDefined(emcMotion.queueFull, emcTraj.queueFull),
id: firstDefined(emcTraj.id, emcMotion.motionId),
motionId: firstDefined(emcMotion.motionId, emcTraj.id),
paused: firstDefined(emcTraj.paused, emcMotion.paused),
stepping: firstDefined(emcTraj.singleStepping, emcMotion.stepping),
currentVel: firstDefined(emcTraj.currentVel, emcMotion.currentVel),
programLine: firstDefined(emcMotion.programLine, task.motionLine),
};
}
function normalizeStatusAxis(emcMotion = {}) {
const axisByName = emcMotion.axisByName || {};
const actualPosition = emcMotion.traj?.actualPosition || {};
const position = emcMotion.traj?.position || {};
const axisArray = Array.isArray(emcMotion.axis) ? emcMotion.axis : [];
const byLetter = new Map(axisArray.map((axis) => [String(axis.letter || "").toLowerCase(), axis]));
return {
x: firstDefined(axisByName.x, actualPosition.x, byLetter.get("x")?.posFb, position.x),
y: firstDefined(axisByName.y, actualPosition.y, byLetter.get("y")?.posFb, position.y),
z: firstDefined(axisByName.z, actualPosition.z, byLetter.get("z")?.posFb, position.z),
a: firstDefined(axisByName.a, actualPosition.a, byLetter.get("a")?.posFb, position.a),
b: firstDefined(axisByName.b, actualPosition.b, byLetter.get("b")?.posFb, position.b),
c: firstDefined(axisByName.c, actualPosition.c, byLetter.get("c")?.posFb, position.c),
};
}
function firstDefined(...values) {
return values.find((value) => value !== undefined && value !== null);
}
function normalizeTaskHalHomed(value, allHomed = false) {
if (Array.isArray(value)) {
return value.map(Boolean);

View File

@@ -80,28 +80,28 @@ export function createLinuxCncTaskPolicyStatus(state) {
const machineFileOpened = profileId === "gmoccapy-xyzab"
? Boolean(machineFileStaging.selectedGcodeSourceRel)
: Boolean(machineFileStaging.selectedGcodeSourceRel || state.activeProgram);
const taskProgramOpen = Boolean(state.taskHalStatus?.ui?.programOpen || state.taskHalStatus?.task?.programOpen);
const taskMotionPlanLoaded = Boolean(state.taskHalStatus?.ui?.motionPlanLoaded || state.taskHalStatus?.task?.motionPlanLoaded);
const taskProgramOpen = Boolean(state.taskHalStatus?.ui?.programOpen || state.taskHalStatus?.emcStatus?.task?.programOpen);
const taskMotionPlanLoaded = Boolean(state.taskHalStatus?.ui?.motionPlanLoaded || state.taskHalStatus?.emcStatus?.task?.motionPlanLoaded);
const interpIdle = interpState === "idle";
const motionPaused = state.machine?.motionPaused === true ||
state.taskHalStatus?.ui?.motionPaused === true ||
state.taskHalStatus?.motionStatus?.motion?.paused === true;
state.taskHalStatus?.emcStatus?.motion?.paused === true;
const taskPaused = state.machine?.taskPaused === true ||
state.taskHalStatus?.ui?.taskPaused === true ||
state.taskHalStatus?.task?.taskPaused === true;
state.taskHalStatus?.emcStatus?.task?.taskPaused === true;
const motionEnabled = taskState === "on" ||
state.machine?.motionEnabled === true ||
state.taskHalStatus?.ui?.motionEnabled === true ||
state.taskHalStatus?.motionStatus?.motion?.enabled === true;
state.taskHalStatus?.emcStatus?.motion?.enabled === true;
const singleStepping = state.machine?.singleStepping === true ||
state.taskHalStatus?.ui?.singleStepping === true ||
state.taskHalStatus?.task?.singleStepping === true;
state.taskHalStatus?.emcStatus?.task?.singleStepping === true;
const motionStepping = state.machine?.motionStepping === true ||
state.taskHalStatus?.ui?.motionStepping === true ||
state.taskHalStatus?.motionStatus?.motion?.stepping === true;
state.taskHalStatus?.emcStatus?.motion?.stepping === true;
const resumeInhibit = state.machine?.resumeInhibit === true ||
state.taskHalStatus?.ui?.resumeInhibit === true ||
state.taskHalStatus?.task?.resumeInhibit === true;
state.taskHalStatus?.emcStatus?.task?.resumeInhibit === true;
const feedbackPaused = state.programRuntimeFeedback?.paused === true;
const paused = motionPaused || taskPaused || feedbackPaused || interpState === "paused" || state.runState === "paused";
const running = taskMode === "auto" &&

View File

@@ -1625,7 +1625,11 @@ export function createSimulationStore(seed = {}) {
], {
operatorMessage: `task/HAL MDI ${command}`,
preserveAxisPose: mdiPatch.axisPose,
preserveMachine: mdiPatch.machine,
preserveMachine: {
...mdiPatch.machine,
kinsType: mdiPatch.kinsType,
rtcpState: mdiPatch.rtcpState,
},
}).catch(() => {});
break;
}
@@ -3459,7 +3463,7 @@ function createLinuxCncProcessMonitor(state = {}) {
const uiExecution = state.programUiExecution || createProgramUiExecution(state);
const status = state.taskHalStatus || {};
const ui = status.ui || {};
const motion = status.motionStatus?.motion || {};
const motion = taskHalMotion(status);
const halPins = status.halSnapshot?.pins || {};
const toolRuntime = state.toolRuntimeState || {};
const toolDb = state.toolDbSimulation || {};
@@ -3487,7 +3491,7 @@ function createLinuxCncProcessMonitor(state = {}) {
? 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 requestedVelocity = Number(feedback.requestedVelocityMmPerMin ?? (Number(motion.requestedVel || 0) * 60) ?? currentVelocity);
const feedRate = Number(state.feed?.feedRate || currentVelocity || 0);
const spindleCommandRpm = Number(state.spindle?.rpm || 0);
const spindleActualRpm = state.spindle?.enabled
@@ -3929,11 +3933,13 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
preserveMachine = null,
preserveAxisPose = null,
} = {}) {
rejectLegacyTaskHalStatusFields(status);
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
const statusInterpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const statusMotionPaused = ui.motionPaused === true || motion.paused === true;
const statusMotionPaused = ui.motionPaused === true || motion.paused === true || traj.paused === true;
const statusTaskPaused = ui.taskPaused === true || task.taskPaused === true;
const statusPaused = statusInterpState === "paused" || statusMotionPaused || statusTaskPaused;
const rawTaskState = normalizeTaskHalTaskState(ui.taskState || task.state);
@@ -3981,7 +3987,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
(allHomed ? "homed" : "unhomed"),
);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const kinsType = resolveTaskHalKinsType(state, status, activeLine, preserveMachine?.kinsType);
const motionPaused = statusMotionPaused || preserveMachine?.motionPaused === true;
const taskPaused = statusTaskPaused || preserveMachine?.taskPaused === true;
const paused = interpState === "paused" || motionPaused || taskPaused;
@@ -3990,6 +3996,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
preserveMachine?.singleStepping === true;
const motionStepping = ui.motionStepping === true ||
motion.stepping === true ||
traj.singleStepping === true ||
preserveMachine?.motionStepping === true;
const interpResumeState = normalizeTaskHalInterpState(
ui.interpResumeState ||
@@ -4166,8 +4173,9 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode);
const aborted = motion.aborted === true;
@@ -4175,6 +4183,7 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
traj.paused === true ||
task.taskPaused === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const complete = interpState === "idle"
@@ -4190,12 +4199,14 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
function isTaskHalStatusPaused(status = {}) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
return normalizeTaskHalInterpState(ui.interpState || task.interpState) === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
traj.paused === true ||
task.taskPaused === true;
}
@@ -4301,7 +4312,10 @@ function createStoppedProgramStatePatch(state, {
};
}
function resolveTaskHalKinsType(state, status, activeLine) {
function resolveTaskHalKinsType(state, status, activeLine, preserveKinsType = null) {
if (preserveKinsType) {
return preserveKinsType;
}
const ui = status?.ui || {};
const numeric = Number(ui.switchkinsType);
if (Number.isFinite(numeric) && numeric !== 0) {
@@ -4381,7 +4395,7 @@ function addAxisDelta(axisPose, delta, profile) {
}
function isJogStatus(status) {
const motion = status?.motionStatus?.motion || {};
const motion = taskHalMotion(status);
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
}
@@ -4394,17 +4408,21 @@ function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const ui = status?.ui || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
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 interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
status?.task?.taskPaused === true;
traj.paused === true ||
task.taskPaused === true;
const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0);
const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60;
const motionInPosition = motion.inPosition === true || traj.inpos === true;
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "linuxcnc-task-motion-hal-wasm",
@@ -4414,7 +4432,7 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
line: activeLine,
motionProgramLine,
halProgramLine,
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "emc-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine,
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
paused,
@@ -4422,16 +4440,32 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
axisPose,
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: requestedVelocity,
distanceToGo: motion.inPosition === true ? 0 : 1,
distanceToGo: motionInPosition ? 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),
queueDepth: Number(ui.motionQueueDepth || motion.commandQueueDepth || traj.queue || 0),
activeDepth: motionInPosition ? 0 : 1,
cycle: Number(ui.servoCycle || 0),
taskCycle: Number(ui.taskCycle || task.cycle || 0),
halChangedPinCount: Number(ui.halChangedPinCount || 0),
};
}
function taskHalTask(status = {}) {
return status?.emcStatus?.task || {};
}
function taskHalMotion(status = {}) {
return status?.emcStatus?.motion || {};
}
function rejectLegacyTaskHalStatusFields(status = {}) {
const legacyFields = ["taskTopLevelStatus", "rcsStatus", "task", "servoCycle", "motionStatus"];
const present = legacyFields.filter((field) => Object.hasOwn(status, field));
if (present.length > 0) {
throw new Error(`Task/HAL status contains removed legacy fields: ${present.join(", ")}`);
}
}
function buildTaskHalProgramMotionPlanFromPreviewPath({
programPath = null,
path = null,

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@
"smoke": "npm run smoke:node",
"smoke:node": "node ../tests/node/verify_xyzbc_trt_web_app.mjs",
"smoke:browser": "bash ../tests/browser/verify_xyzbc_trt_browser.sh",
"smoke:buttons": "bash ../tests/browser/verify_xyzbc_trt_all_buttons.sh",
"evidence:web": "node ../tools/collect-web-xyzbc-trt-evidence.mjs",
"evidence:compare": "node ../tools/compare-xyzbc-trt-evidence.mjs"
},

View File

@@ -212,15 +212,22 @@ export function buildTaskHalProgramMotionPlan({
}
export function normalizeTaskHalStatus(status = {}) {
const motion = status.motionStatus?.motion || {};
const axis = status.motionStatus?.axis || {};
rejectLegacyTaskHalStatusFields(status);
const emcStatus = requireStandaloneEmcStatus(status);
const emcTask = emcStatus.task;
const emcMotion = emcStatus.motion;
const emcTraj = emcMotion.traj || {};
const task = normalizeStatusTask(emcTask);
const axis = normalizeStatusAxis(emcMotion);
const motion = normalizeStatusMotion(emcMotion, emcTraj, task);
const commandQueueDepth = firstDefined(emcMotion.commandQueueDepth, emcTraj.queue, 0);
const statusCycle = firstDefined(emcMotion.cycle, emcTraj.cycle, Number(task.cycle || 0) * 10, 0);
const halPins = status.halSnapshot?.pins || {};
const motionProgramLine = Number(motion.programLine || 0);
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
const motionQueueDepth = Number(
status.motionStatus?.commandQueueDepth ??
commandQueueDepth ??
motion.commandQueueDepth ??
status.motionStatus?.queueDepth ??
motion.queueDepth ??
0,
);
@@ -229,45 +236,49 @@ export function normalizeTaskHalStatus(status = {}) {
: halProgramLine > 0
? halProgramLine
: 1;
const taskRuntimeReady = true;
const taskMotionReady = emcMotion.valid === true;
const halProgramLineReady = Boolean(halPins["motion.program-line"]);
const halRuntimeReady = Boolean(status.halSnapshot?.halRuntimeReady ?? status.halSnapshot?.ready ?? true);
return {
...status,
semanticBoundary: SEMANTIC_BOUNDARY,
summary: {
taskRuntimeReady: status.taskRuntimeReady === true,
motionRuntimeReady: status.motionStatus?.motionHalSyncReady === true || status.taskCommandsDriveMotionRuntime === true,
halRuntimeReady: Boolean(status.halSnapshot?.halRuntimeReady ?? status.halSnapshot?.ready ?? true),
halSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
taskHalComparisonReady: status.taskRuntimeReady === true && status.taskCommandsDriveMotionRuntime === true,
taskRuntimeReady,
motionRuntimeReady: taskMotionReady,
halRuntimeReady,
halSyncReady: taskMotionReady && halProgramLineReady,
taskHalComparisonReady: taskRuntimeReady && taskMotionReady,
switchkinsRemapHalSync: Boolean(halPins["motion.switchkins-type"]),
nativeTaskReady: status.taskRuntimeReady === true,
nativeHalSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
nativeTaskReady: taskRuntimeReady,
nativeHalSyncReady: taskMotionReady && halProgramLineReady,
fullLinuxCncProgramExecutionReady: false,
hardwareDrive: false,
hostRealtimeKernel: false,
},
ui: {
taskState: String(status.task?.state || "ESTOP").toLowerCase(),
taskMode: String(status.task?.mode || "MANUAL").toLowerCase(),
interpState: String(status.task?.interpState || "IDLE").toLowerCase(),
interpResumeState: String(status.task?.interpResumeState || status.task?.interpState || "IDLE").toLowerCase(),
execState: String(status.task?.execState || "DONE").toLowerCase(),
taskPaused: status.task?.taskPaused === true,
singleStepping: status.task?.singleStepping === true,
taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0),
programOpen: status.task?.programOpen === true,
programFile: status.task?.file || status.task?.openProgram || null,
programStartLine: Number(status.task?.programStartLine || 0),
planId: Number(status.task?.planId || 0),
motionPlanLoaded: status.task?.motionPlanLoaded === true,
errorText: status.task?.errorText || null,
allHomed: status.task?.allHomed === true,
homed: normalizeTaskHalHomed(status.task?.homed, status.task?.allHomed === true),
noForceHoming: status.task?.noForceHoming === true,
homing: status.task?.homing === true,
homeState: String(status.task?.homeState || "UNHOMED").toLowerCase(),
motionEnabled: String(status.task?.state || "").toUpperCase() === "ON" || motion.enabled === true,
resumeInhibit: status.task?.resumeInhibit === true,
servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0),
taskState: String(task.state || "ESTOP").toLowerCase(),
taskMode: String(task.mode || "MANUAL").toLowerCase(),
interpState: String(task.interpState || "IDLE").toLowerCase(),
interpResumeState: String(task.interpResumeState || task.interpState || "IDLE").toLowerCase(),
execState: String(task.execState || "DONE").toLowerCase(),
taskPaused: task.taskPaused === true,
singleStepping: task.singleStepping === true,
taskCycle: Number(task.taskCycle ?? 0),
programOpen: task.programOpen === true,
programFile: task.file || task.openProgram || null,
programStartLine: Number(task.programStartLine || 0),
planId: Number(task.planId || 0),
motionPlanLoaded: task.motionPlanLoaded === true,
errorText: task.errorText || null,
allHomed: task.allHomed === true,
homed: normalizeTaskHalHomed(task.homed, task.allHomed === true),
noForceHoming: task.noForceHoming === true,
homing: task.homing === true,
homeState: String(task.homeState || "UNHOMED").toLowerCase(),
motionEnabled: String(task.state || "").toUpperCase() === "ON" || motion.enabled === true,
resumeInhibit: task.resumeInhibit === true,
servoCycle: Number(statusCycle ?? task.servoCycle ?? 0),
motionQueueDepth,
motionPaused: motion.paused === true,
motionStepping: motion.stepping === true,
@@ -279,25 +290,92 @@ export function normalizeTaskHalStatus(status = {}) {
activeLine,
motionProgramLine,
halProgramLine,
activeLineSource: motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
activeLineSource: motionProgramLine > 0 ? "emc-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0
? motionProgramLine === halProgramLine
: false,
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
axisPose: {
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
y: Number(axis.y ?? halPins["axis.1.pos-cmd"]?.value ?? 0),
z: Number(axis.z ?? halPins["axis.2.pos-cmd"]?.value ?? 0),
a: Number(axis.a ?? halPins["axis.3.pos-cmd"]?.value ?? 0),
b: Number(axis.b ?? halPins["axis.4.pos-cmd"]?.value ?? 0),
c: Number(axis.c ?? halPins["axis.5.pos-cmd"]?.value ?? 0),
x: Number(axis.x ?? 0),
y: Number(axis.y ?? 0),
z: Number(axis.z ?? 0),
a: Number(axis.a ?? 0),
b: Number(axis.b ?? 0),
c: Number(axis.c ?? 0),
},
axisPoseFrame: isJogMotion(motion) ? "task-local" : "work",
currentVelocity: Number(motion.currentVel || motion.currentVelocity || 0) * 60,
currentVelocity: Number(motion.currentVel ?? motion.currentVelocity ?? 0) * 60,
},
};
}
function rejectLegacyTaskHalStatusFields(status = {}) {
const legacyFields = ["taskTopLevelStatus", "rcsStatus", "task", "servoCycle", "motionStatus"];
const present = legacyFields.filter((field) => Object.hasOwn(status, field));
if (present.length > 0) {
throw new Error(`Task/HAL status contains removed legacy fields: ${present.join(", ")}`);
}
}
function requireStandaloneEmcStatus(status = {}) {
const emcStatus = status.emcStatus;
if (!emcStatus || typeof emcStatus !== "object") {
throw new Error("Task/HAL status requires StandaloneEmcStatus emcStatus");
}
if (!emcStatus.task || typeof emcStatus.task !== "object") {
throw new Error("Task/HAL status requires StandaloneEmcStatus emcStatus.task");
}
if (!emcStatus.motion || typeof emcStatus.motion !== "object") {
throw new Error("Task/HAL status requires StandaloneEmcStatus emcStatus.motion");
}
return emcStatus;
}
function normalizeStatusTask(emcTask = {}) {
return {
...emcTask,
taskCycle: firstDefined(emcTask.taskCycle, emcTask.cycle, 0),
};
}
function normalizeStatusMotion(emcMotion = {}, emcTraj = {}, task = {}) {
return {
...emcMotion,
enabled: firstDefined(emcMotion.enabled, emcTraj.enabled),
inPosition: firstDefined(emcMotion.inPosition, emcTraj.inpos),
queueDepth: firstDefined(emcMotion.queueDepth, emcTraj.queue),
commandQueueDepth: firstDefined(emcMotion.commandQueueDepth, emcTraj.queue),
activeDepth: firstDefined(emcMotion.activeDepth, emcTraj.activeQueue),
queueFull: firstDefined(emcMotion.queueFull, emcTraj.queueFull),
id: firstDefined(emcTraj.id, emcMotion.motionId),
motionId: firstDefined(emcMotion.motionId, emcTraj.id),
paused: firstDefined(emcTraj.paused, emcMotion.paused),
stepping: firstDefined(emcTraj.singleStepping, emcMotion.stepping),
currentVel: firstDefined(emcTraj.currentVel, emcMotion.currentVel),
programLine: firstDefined(emcMotion.programLine, task.motionLine),
};
}
function normalizeStatusAxis(emcMotion = {}) {
const axisByName = emcMotion.axisByName || {};
const actualPosition = emcMotion.traj?.actualPosition || {};
const position = emcMotion.traj?.position || {};
const axisArray = Array.isArray(emcMotion.axis) ? emcMotion.axis : [];
const byLetter = new Map(axisArray.map((axis) => [String(axis.letter || "").toLowerCase(), axis]));
return {
x: firstDefined(axisByName.x, actualPosition.x, byLetter.get("x")?.posFb, position.x),
y: firstDefined(axisByName.y, actualPosition.y, byLetter.get("y")?.posFb, position.y),
z: firstDefined(axisByName.z, actualPosition.z, byLetter.get("z")?.posFb, position.z),
a: firstDefined(axisByName.a, actualPosition.a, byLetter.get("a")?.posFb, position.a),
b: firstDefined(axisByName.b, actualPosition.b, byLetter.get("b")?.posFb, position.b),
c: firstDefined(axisByName.c, actualPosition.c, byLetter.get("c")?.posFb, position.c),
};
}
function firstDefined(...values) {
return values.find((value) => value !== undefined && value !== null);
}
function normalizeTaskHalHomed(value, allHomed = false) {
if (Array.isArray(value)) {
return value.map(Boolean);

View File

@@ -80,28 +80,28 @@ export function createLinuxCncTaskPolicyStatus(state) {
const machineFileOpened = profileId === "gmoccapy-xyzab"
? Boolean(machineFileStaging.selectedGcodeSourceRel)
: Boolean(machineFileStaging.selectedGcodeSourceRel || state.activeProgram);
const taskProgramOpen = Boolean(state.taskHalStatus?.ui?.programOpen || state.taskHalStatus?.task?.programOpen);
const taskMotionPlanLoaded = Boolean(state.taskHalStatus?.ui?.motionPlanLoaded || state.taskHalStatus?.task?.motionPlanLoaded);
const taskProgramOpen = Boolean(state.taskHalStatus?.ui?.programOpen || state.taskHalStatus?.emcStatus?.task?.programOpen);
const taskMotionPlanLoaded = Boolean(state.taskHalStatus?.ui?.motionPlanLoaded || state.taskHalStatus?.emcStatus?.task?.motionPlanLoaded);
const interpIdle = interpState === "idle";
const motionPaused = state.machine?.motionPaused === true ||
state.taskHalStatus?.ui?.motionPaused === true ||
state.taskHalStatus?.motionStatus?.motion?.paused === true;
state.taskHalStatus?.emcStatus?.motion?.paused === true;
const taskPaused = state.machine?.taskPaused === true ||
state.taskHalStatus?.ui?.taskPaused === true ||
state.taskHalStatus?.task?.taskPaused === true;
state.taskHalStatus?.emcStatus?.task?.taskPaused === true;
const motionEnabled = taskState === "on" ||
state.machine?.motionEnabled === true ||
state.taskHalStatus?.ui?.motionEnabled === true ||
state.taskHalStatus?.motionStatus?.motion?.enabled === true;
state.taskHalStatus?.emcStatus?.motion?.enabled === true;
const singleStepping = state.machine?.singleStepping === true ||
state.taskHalStatus?.ui?.singleStepping === true ||
state.taskHalStatus?.task?.singleStepping === true;
state.taskHalStatus?.emcStatus?.task?.singleStepping === true;
const motionStepping = state.machine?.motionStepping === true ||
state.taskHalStatus?.ui?.motionStepping === true ||
state.taskHalStatus?.motionStatus?.motion?.stepping === true;
state.taskHalStatus?.emcStatus?.motion?.stepping === true;
const resumeInhibit = state.machine?.resumeInhibit === true ||
state.taskHalStatus?.ui?.resumeInhibit === true ||
state.taskHalStatus?.task?.resumeInhibit === true;
state.taskHalStatus?.emcStatus?.task?.resumeInhibit === true;
const feedbackPaused = state.programRuntimeFeedback?.paused === true;
const paused = motionPaused || taskPaused || feedbackPaused || interpState === "paused" || state.runState === "paused";
const running = taskMode === "auto" &&

View File

@@ -1625,7 +1625,11 @@ export function createSimulationStore(seed = {}) {
], {
operatorMessage: `task/HAL MDI ${command}`,
preserveAxisPose: mdiPatch.axisPose,
preserveMachine: mdiPatch.machine,
preserveMachine: {
...mdiPatch.machine,
kinsType: mdiPatch.kinsType,
rtcpState: mdiPatch.rtcpState,
},
}).catch(() => {});
break;
}
@@ -3459,7 +3463,7 @@ function createLinuxCncProcessMonitor(state = {}) {
const uiExecution = state.programUiExecution || createProgramUiExecution(state);
const status = state.taskHalStatus || {};
const ui = status.ui || {};
const motion = status.motionStatus?.motion || {};
const motion = taskHalMotion(status);
const halPins = status.halSnapshot?.pins || {};
const toolRuntime = state.toolRuntimeState || {};
const toolDb = state.toolDbSimulation || {};
@@ -3487,7 +3491,7 @@ function createLinuxCncProcessMonitor(state = {}) {
? 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 requestedVelocity = Number(feedback.requestedVelocityMmPerMin ?? (Number(motion.requestedVel || 0) * 60) ?? currentVelocity);
const feedRate = Number(state.feed?.feedRate || currentVelocity || 0);
const spindleCommandRpm = Number(state.spindle?.rpm || 0);
const spindleActualRpm = state.spindle?.enabled
@@ -3929,11 +3933,13 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
preserveMachine = null,
preserveAxisPose = null,
} = {}) {
rejectLegacyTaskHalStatusFields(status);
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
const statusInterpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const statusMotionPaused = ui.motionPaused === true || motion.paused === true;
const statusMotionPaused = ui.motionPaused === true || motion.paused === true || traj.paused === true;
const statusTaskPaused = ui.taskPaused === true || task.taskPaused === true;
const statusPaused = statusInterpState === "paused" || statusMotionPaused || statusTaskPaused;
const rawTaskState = normalizeTaskHalTaskState(ui.taskState || task.state);
@@ -3981,7 +3987,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
(allHomed ? "homed" : "unhomed"),
);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const kinsType = resolveTaskHalKinsType(state, status, activeLine, preserveMachine?.kinsType);
const motionPaused = statusMotionPaused || preserveMachine?.motionPaused === true;
const taskPaused = statusTaskPaused || preserveMachine?.taskPaused === true;
const paused = interpState === "paused" || motionPaused || taskPaused;
@@ -3990,6 +3996,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
preserveMachine?.singleStepping === true;
const motionStepping = ui.motionStepping === true ||
motion.stepping === true ||
traj.singleStepping === true ||
preserveMachine?.motionStepping === true;
const interpResumeState = normalizeTaskHalInterpState(
ui.interpResumeState ||
@@ -4166,8 +4173,9 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode);
const aborted = motion.aborted === true;
@@ -4175,6 +4183,7 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
traj.paused === true ||
task.taskPaused === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const complete = interpState === "idle"
@@ -4190,12 +4199,14 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
function isTaskHalStatusPaused(status = {}) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
return normalizeTaskHalInterpState(ui.interpState || task.interpState) === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
traj.paused === true ||
task.taskPaused === true;
}
@@ -4301,7 +4312,10 @@ function createStoppedProgramStatePatch(state, {
};
}
function resolveTaskHalKinsType(state, status, activeLine) {
function resolveTaskHalKinsType(state, status, activeLine, preserveKinsType = null) {
if (preserveKinsType) {
return preserveKinsType;
}
const ui = status?.ui || {};
const numeric = Number(ui.switchkinsType);
if (Number.isFinite(numeric) && numeric !== 0) {
@@ -4381,7 +4395,7 @@ function addAxisDelta(axisPose, delta, profile) {
}
function isJogStatus(status) {
const motion = status?.motionStatus?.motion || {};
const motion = taskHalMotion(status);
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
}
@@ -4394,17 +4408,21 @@ function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const ui = status?.ui || {};
const motion = status?.motionStatus?.motion || {};
const task = taskHalTask(status);
const motion = taskHalMotion(status);
const traj = motion.traj || {};
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 interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
status?.task?.taskPaused === true;
traj.paused === true ||
task.taskPaused === true;
const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0);
const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60;
const motionInPosition = motion.inPosition === true || traj.inpos === true;
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "linuxcnc-task-motion-hal-wasm",
@@ -4414,7 +4432,7 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
line: activeLine,
motionProgramLine,
halProgramLine,
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "emc-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine,
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
paused,
@@ -4422,16 +4440,32 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
axisPose,
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: requestedVelocity,
distanceToGo: motion.inPosition === true ? 0 : 1,
distanceToGo: motionInPosition ? 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),
queueDepth: Number(ui.motionQueueDepth || motion.commandQueueDepth || traj.queue || 0),
activeDepth: motionInPosition ? 0 : 1,
cycle: Number(ui.servoCycle || 0),
taskCycle: Number(ui.taskCycle || task.cycle || 0),
halChangedPinCount: Number(ui.halChangedPinCount || 0),
};
}
function taskHalTask(status = {}) {
return status?.emcStatus?.task || {};
}
function taskHalMotion(status = {}) {
return status?.emcStatus?.motion || {};
}
function rejectLegacyTaskHalStatusFields(status = {}) {
const legacyFields = ["taskTopLevelStatus", "rcsStatus", "task", "servoCycle", "motionStatus"];
const present = legacyFields.filter((field) => Object.hasOwn(status, field));
if (present.length > 0) {
throw new Error(`Task/HAL status contains removed legacy fields: ${present.join(", ")}`);
}
}
function buildTaskHalProgramMotionPlanFromPreviewPath({
programPath = null,
path = null,