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,

View File

@@ -0,0 +1,67 @@
{
"apiName": "web-rtcp-5axis-native-task-hal-readiness-audit",
"batch": "M18-native-task-hal-source-and-artifact-audit",
"generatedAt": "2026-06-22T00:00:00.000Z",
"status": "ok",
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"promotionScope": "web_simulation_only",
"taskHalWebSimulationBoundaryConsistent": true,
"webSimulation": {
"promoted": true,
"taskRuntimeReady": true,
"motionRuntimeReady": true,
"halRuntimeReady": true,
"nativeTaskReady": true,
"nativeHalSyncReady": true,
"fullLinuxCncProgramExecutionReady": true,
"promotionAllowed": true
},
"nativeHostAndHardware": {
"nativeProbe": "ok",
"nativeProbeStatus": "ready_disabled_by_default",
"nativePromotionAllowed": false,
"hardwareDrive": false,
"hostRealtimeKernel": false,
"externalUserMProcessReady": false,
"toolDbProcessReady": false,
"hostExternalUserMProcessReady": false,
"hostToolDbProcessReady": false,
"arbitraryUserMExecution": false
},
"toolUserWebSimulation": {
"externalUserMProcessReady": true,
"externalUserMProcessScope": "web_simulation_only",
"toolDbProcessReady": true,
"toolDbProcessScope": "web_simulation_only",
"ready": true
},
"sourceManifest": {
"ready": true,
"taskSourceCount": 7,
"halSourceCount": 4,
"motionSourceCount": 6,
"nmlSourceCount": 1,
"libnmlSourceCount": 2,
"referenceSourceReady": true,
"vendorSourceReady": false,
"vendorHashMatchReady": false
},
"gates": {
"task_hal_web_simulation_boundary_consistent": 1,
"native_task_hal_host_probe_status": "ready_disabled_by_default",
"hardware_drive": 0,
"host_realtime_kernel": 0,
"external_user_m_process_ready": 1,
"tool_db_process_ready": 1,
"host_external_user_m_process_ready": 0,
"host_tool_db_process_ready": 0,
"promotion_scope": "web_simulation_only"
},
"artifacts": {
"readinessJson": "web-rtcp-5axis-xyzbc-trt-sim-plan/build/readiness/native-task-hal-readiness.json",
"sourceManifestLog": "wasm-port/build/task-hal/verify_task_hal_source_manifest.stdout.log",
"sourceManifestReport": "wasm-port/build/task-hal/task-hal-source-manifest.tsv",
"nativeProbeLog": "wasm-port/build/task-hal/probe_trt_task_hal_runtime.stdout.log"
},
"blockers": []
}

View File

@@ -0,0 +1,63 @@
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
const rootDir = path.resolve(import.meta.dirname, "../../..");
const projectDir = path.join(rootDir, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const requireFromApp = createRequire(path.join(projectDir, "app/package.json"));
const { chromium } = requireFromApp("playwright");
const server = createServer(async (request, response) => {
try {
const url = new URL(request.url || "/", "http://127.0.0.1");
const pathname = decodeURIComponent(url.pathname);
const filePath = path.join(rootDir, pathname);
if (!filePath.startsWith(rootDir)) {
response.writeHead(403).end("forbidden");
return;
}
const body = await readFile(filePath);
response.writeHead(200, { "content-type": contentType(filePath) });
response.end(body);
} catch (error) {
response.writeHead(404).end(error instanceof Error ? error.message : String(error));
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
const browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM || undefined,
args: ["--disable-gpu", "--no-sandbox", "--enable-unsafe-swiftshader"],
});
try {
const page = await browser.newPage();
const testUrl = `http://127.0.0.1:${port}/web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/xyzbc_trt_all_buttons.html?app=../../app/dist/index.html`;
await page.goto(testUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForFunction(() => {
const text = document.querySelector("#result")?.textContent || "";
return text.startsWith("xyzbc_trt_all_buttons=ok") || text.startsWith("xyzbc_trt_all_buttons=fail");
}, null, { timeout: 120000 });
const result = await page.locator("#result").textContent();
if (!result?.startsWith("xyzbc_trt_all_buttons=ok")) {
throw new Error(result || "all button test did not report a result");
}
console.log(result.trim());
} finally {
await browser.close();
await new Promise((resolve) => server.close(resolve));
}
function contentType(filePath) {
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) return "text/javascript; charset=utf-8";
if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
if (filePath.endsWith(".json")) return "application/json; charset=utf-8";
if (filePath.endsWith(".wasm")) return "application/wasm";
if (filePath.endsWith(".svg")) return "image/svg+xml";
if (filePath.endsWith(".png")) return "image/png";
return "application/octet-stream";
}

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
PROJECT_DIR="$ROOT_DIR/web-rtcp-5axis-xyzbc-trt-sim-plan"
npm --prefix "$PROJECT_DIR/app" run build >/dev/null
node "$PROJECT_DIR/tests/browser/verify_xyzbc_trt_all_buttons.mjs"

View File

@@ -0,0 +1,344 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>xyzbc-trt all button coverage</title>
</head>
<body>
<pre id="result">xyzbc_trt_all_buttons=pending</pre>
<iframe id="app-frame" title="xyzbc-trt app"></iframe>
<script type="module">
const result = document.querySelector("#result");
const frame = document.querySelector("#app-frame");
const appSrc = new URLSearchParams(window.location.search).get("app") || "../../app/index.html";
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function runAllButtons() {
result.textContent = "xyzbc_trt_all_buttons=loading-frame";
const frameLoaded = new Promise((resolve, reject) => {
frame.addEventListener("load", resolve, { once: true });
frame.addEventListener("error", reject, { once: true });
});
frame.src = appSrc;
await frameLoaded;
result.textContent = "xyzbc_trt_all_buttons=waiting-api";
const win = frame.contentWindow;
const doc = frame.contentDocument;
const runtimeErrors = [];
win.addEventListener("error", (event) => {
runtimeErrors.push(event.message || String(event.error || "window error"));
});
win.addEventListener("unhandledrejection", (event) => {
runtimeErrors.push(event.reason?.message || String(event.reason || "unhandled rejection"));
});
await waitFor(() => win.webRtcp5AxisSimulation, "public simulation API");
const api = win.webRtcp5AxisSimulation;
result.textContent = "xyzbc_trt_all_buttons=waiting-runtimes";
await waitFor(() => {
const state = api.getState();
return state.kinematicsRuntime?.loaded === true &&
state.interpreterRuntime?.loaded === true &&
state.taskHalRuntime?.loaded === true;
}, "runtime loaded state");
result.textContent = "xyzbc_trt_all_buttons=waiting-program";
if (api.getState().machineFileStaging?.status !== "staged") {
api.dispatch({ type: "STAGE_MACHINE_FILES_REQUEST" });
}
await waitFor(() => api.getState().machineFileStaging?.status === "staged", "machine file staging");
const source = api.getState().machineFileStaging?.gcodeSources?.find((item) => item.filename === "xyzbc_switchkins.ngc") ||
api.getState().machineFileStaging?.gcodeSources?.[0];
if (source?.sourceRel) {
api.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: source.sourceRel });
}
await waitFor(() => {
const state = api.getState();
return state.machineFileStaging?.status === "staged" &&
!state.interpreterExecutionPending &&
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0;
}, "staged default program");
result.textContent = "xyzbc_trt_all_buttons=checking-dom";
assertButtonParityDomCoverage(doc, api.getButtonParity());
result.textContent = "xyzbc_trt_all_buttons=checking-menu";
await verifyMenuButtons(doc, api);
result.textContent = "xyzbc_trt_all_buttons=checking-toolbar";
await verifyToolbarButtons(doc, api);
result.textContent = "xyzbc_trt_all_buttons=checking-manual-mdi-pyvcp";
await verifyManualMdiPyvcpButtons(doc, api);
result.textContent = "xyzbc_trt_all_buttons=checking-overrides";
await verifyOverrideCoolantAndViewButtons(doc, api);
if (runtimeErrors.length > 0) {
throw new Error(`runtime errors: ${runtimeErrors.join(" | ")}`);
}
result.textContent = `xyzbc_trt_all_buttons=ok checked=${api.getButtonParity().length}`;
}
function assertButtonParityDomCoverage(doc, buttonParity) {
if (!Array.isArray(buttonParity) || buttonParity.length < 50) {
throw new Error(`button parity list too small: ${buttonParity?.length}`);
}
const controls = [...doc.querySelectorAll("[data-action], [data-menu-command]")];
for (const item of buttonParity) {
const matches = controls.filter((control) => (
control.dataset.action === item.action ||
control.dataset.menuCommand === item.action
));
if (matches.length === 0) {
throw new Error(`button parity item has no DOM control: ${item.id}/${item.action}`);
}
if (!matches.some((control) => control.dataset.axisSourceRef && control.dataset.axisExpectedEffect)) {
throw new Error(`button parity item has no source-tagged DOM control: ${item.id}/${item.action}`);
}
}
}
async function verifyMenuButtons(doc, api) {
await click(doc, '[data-menu-command="stage"]', "menu Stage LinuxCNC files");
await waitState(api, (state) => state.machineFileStaging?.status === "staged", "menu stage completed");
await click(doc, '[data-menu-command="save-session"]', "menu Save session");
await waitState(api, (state) => (
state.sessionPersistence?.status === "saved" &&
Boolean(state.sessionPersistence?.path) &&
Boolean(state.sessionPersistence?.savedAt)
), "menu save session completed");
await click(doc, '[data-menu-command="restore-session"]', "menu Restore session");
await waitState(api, (state) => (
state.sessionPersistence?.status === "restored" &&
Boolean(state.sessionPersistence?.path) &&
Boolean(state.sessionPersistence?.restoredAt)
), "menu restore session completed");
await click(doc, '[data-menu-command="reload"]', "menu Reload");
await waitState(api, (state) => Number(state.programAxisPreviewPath?.sampleCount || 0) > 0, "menu reload restored preview");
await click(doc, '[data-menu-command="view-z"]', "menu View Z");
await waitState(api, (state) => state.preview.selectedView === "z", "menu view z");
await click(doc, '[data-menu-command="view-y"]', "menu View Y");
await waitState(api, (state) => state.preview.selectedView === "y", "menu view y");
await click(doc, '[data-menu-command="view-x"]', "menu View X");
await waitState(api, (state) => state.preview.selectedView === "x", "menu view x");
await click(doc, '[data-menu-command="view-p"]', "menu View P");
await waitState(api, (state) => state.preview.selectedView === "iso", "menu view p");
await click(doc, '[data-menu-command="clear-preview"]', "menu Clear Live Plot");
await waitState(api, (state) => state.preview.pathPoints === 0, "menu clear preview");
await click(doc, '[data-menu-command="audit"]', "menu Run parity audit");
await waitState(api, (state) => (
state.interpreterExecutionPending === false &&
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0 &&
state.taskHalRuntime?.loaded === true
), "menu audit completed observable work");
}
async function verifyToolbarButtons(doc, api) {
await ensureResetPoweredHomedManual(doc, api);
await click(doc, '[data-action="toggle-auto-manual"]', "toolbar AUTO");
await waitState(api, (state) => state.machine.mode === "auto", "toolbar auto");
await click(doc, '[data-action="toggle-auto-manual"]', "toolbar MANUAL");
await waitState(api, (state) => state.machine.mode === "manual", "toolbar manual");
await click(doc, '[data-action="reload"]', "toolbar reload");
await waitState(api, (state) => Number(state.programAxisPreviewPath?.sampleCount || 0) > 0, "toolbar reload");
await click(doc, '[data-action="run"]', "toolbar run");
await waitState(api, (state) => state.runState === "running" || state.runState === "complete", "toolbar run started");
await click(doc, '[data-tool-id="tbtn_pause"]', "toolbar pause");
await waitState(api, (state) => state.runState === "paused", "toolbar pause");
await click(doc, '[data-action="step"]', "toolbar step");
await waitState(api, (state) => state.machine.taskPaused === true && Number(state.programExecutionSampleIndex) >= 0, "toolbar step");
await click(doc, '[data-tool-id="tbtn_pause"]', "toolbar resume");
await waitState(api, (state) => state.runState === "running", "toolbar resume");
await click(doc, '[data-action="stop"]', "toolbar stop");
await waitState(api, (state) => state.runState === "stopped", "toolbar stop");
await click(doc, '[data-action="estop"]', "toolbar estop");
await waitState(api, (state) => state.machine.estopActive === true, "toolbar estop");
await click(doc, '[data-action="estop"]', "toolbar reset");
await waitState(api, (state) => state.machine.estopActive === false, "toolbar reset");
if (api.getState().machine.powerOn !== true) {
await click(doc, '[data-action="power"]', "toolbar power on after reset");
await waitState(api, (state) => state.machine.powerOn === true, "toolbar power on after reset");
}
await click(doc, '[data-action="power"]', "toolbar power off");
await waitState(api, (state) => state.machine.powerOn === false, "toolbar power off");
await click(doc, '[data-action="power"]', "toolbar power on");
await waitState(api, (state) => state.machine.powerOn === true, "toolbar power on");
}
async function verifyManualMdiPyvcpButtons(doc, api) {
await ensureResetPoweredHomedManual(doc, api);
const joint3 = doc.querySelector('[data-action="select-joint"][value="3"]');
joint3.checked = true;
joint3.dispatchEvent(new Event("change", { bubbles: true }));
await waitState(api, (state) => state.machine.selectedJoint === 3 && state.machine.jogAxis === "b", "select B joint");
const jogIncrement = doc.querySelector('[data-action="jog-increment"]');
jogIncrement.value = "0.1";
jogIncrement.dispatchEvent(new Event("change", { bubbles: true }));
await waitState(api, (state) => Number(state.machine.jogIncrement) === 0.1, "jog increment");
const beforeJogB = api.getState().axisPose.b;
await click(doc, '[data-action="jog-plus"]', "manual jog plus");
await waitState(api, (state) => state.axisPose.b > beforeJogB, "manual jog plus changed B");
const afterJogPlusB = api.getState().axisPose.b;
await click(doc, '[data-action="jog-minus"]', "manual jog minus");
await waitState(api, (state) => state.axisPose.b < afterJogPlusB, "manual jog minus changed B");
await click(doc, '[data-action="touch-off"]', "manual touch off");
await waitState(api, (state) => state.mdiHistory[0] === "G10 L20 P0 B0" && state.machine.mode === "manual", "manual touch off");
await click(doc, '[data-action="tool-touch-off"]', "manual tool touch off");
await waitState(api, (state) => state.mdiHistory[0] === "G43" && state.machine.mode === "manual", "manual tool touch off");
await click(doc, '[data-action="tab-mdi"]', "MDI tab");
await waitState(api, (state) => state.machine.mode === "mdi", "mdi tab");
const mdiInput = doc.querySelector('[data-action="mdi-input"]');
mdiInput.value = "G90";
mdiInput.dispatchEvent(new Event("input", { bubbles: true }));
doc.querySelector('[data-action="mdi-form"]').dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
await waitState(api, (state) => state.mdiHistory[0] === "G90", "mdi submit");
await click(doc, '[data-action="mdi-history"][data-command="G90"]', "MDI history G90");
await waitState(api, (state) => state.mdiHistory[0] === "G90", "mdi history");
await click(doc, '[data-action="kins-tcp"]', "PyVCP TCP");
await waitState(api, (state) => state.kinsType === "tcp-xyzbc" && state.mdiHistory[0] === "M428", "pyvcp tcp");
await click(doc, '[data-action="kins-identity"]', "PyVCP identity");
await waitState(api, (state) => state.kinsType === "identity" && state.mdiHistory[0] === "M429", "pyvcp identity");
await click(doc, '[data-action="kins-userk"]', "PyVCP userk");
await waitState(api, (state) => state.kinsType === "userk" && state.mdiHistory[0] === "M430", "pyvcp userk");
}
async function verifyOverrideCoolantAndViewButtons(doc, api) {
await ensureResetPoweredHomedManual(doc, api);
const feedBefore = api.getState().feed.feedOverride;
await click(doc, '[data-action="feed-override-up"]', "feed override up");
await waitState(api, (state) => state.feed.feedOverride === Math.min(feedBefore + 10, 200), "feed override up");
await click(doc, '[data-action="feed-override-down"]', "feed override down");
await waitState(api, (state) => state.feed.feedOverride === feedBefore, "feed override down");
const rapidBefore = api.getState().feed.rapidOverride;
await click(doc, '[data-action="rapid-override-up"]', "rapid override up");
await waitState(api, (state) => state.feed.rapidOverride === Math.min(rapidBefore + 10, 200), "rapid override up");
await click(doc, '[data-action="rapid-override-down"]', "rapid override down");
await waitState(api, (state) => state.feed.rapidOverride === rapidBefore, "rapid override down");
const spindleOverrideBefore = api.getState().spindle.override;
await click(doc, '[data-action="spindle-override-up"]', "spindle override up");
await waitState(api, (state) => state.spindle.override === Math.min(spindleOverrideBefore + 10, 150), "spindle override up");
await click(doc, '[data-action="spindle-override-down"]', "spindle override down");
await waitState(api, (state) => state.spindle.override === spindleOverrideBefore, "spindle override down");
await click(doc, '[data-action="spindle-reverse"]', "spindle reverse");
await waitState(api, (state) => state.spindle.direction === "reverse" && state.spindle.enabled === true, "spindle reverse");
await click(doc, '[data-action="spindle-stop"]', "spindle stop");
await waitState(api, (state) => state.spindle.direction === "stop" && state.spindle.enabled === false, "spindle stop");
await click(doc, '[data-action="spindle-forward"]', "spindle forward");
await waitState(api, (state) => state.spindle.direction === "forward" && state.spindle.enabled === true, "spindle forward");
await click(doc, '[data-action="toggle-flood"]', "toggle flood");
await waitState(api, (state) => state.coolant.flood === true, "toggle flood");
await click(doc, '[data-action="toggle-mist"]', "toggle mist");
await waitState(api, (state) => state.coolant.mist === true, "toggle mist");
await click(doc, '[data-action="block-delete"]', "block delete");
await waitState(api, (state) => state.gmoccapyGui.optionalBlocks === true, "block delete");
await click(doc, '[data-action="optional-stop"]', "optional stop");
await waitState(api, (state) => state.gmoccapyGui.optionalStop === true, "optional stop");
await click(doc, '[data-action="ignore-limits"]', "ignore limits");
await waitState(api, (state) => state.gmoccapyGui.ignoreLimits === true, "ignore limits");
await click(doc, '[data-action="view-z"]', "view z");
await waitState(api, (state) => state.preview.selectedView === "z", "view z");
await click(doc, '[data-action="view-y"]', "view y");
await waitState(api, (state) => state.preview.selectedView === "y", "view y");
await click(doc, '[data-action="view-x"]', "view x");
await waitState(api, (state) => state.preview.selectedView === "x", "view x");
await click(doc, '[data-action="view-p"]', "view p");
await waitState(api, (state) => state.preview.selectedView === "iso", "view p");
await click(doc, '[data-action="clear-preview"]', "clear preview");
await waitState(api, (state) => state.preview.pathPoints === 0, "clear preview");
await click(doc, '[data-action="reload"]', "reload after clear preview");
await waitState(api, (state) => state.preview.pathPoints > 0, "reload after clear preview");
}
async function ensureResetPoweredHomedManual(doc, api) {
let state = api.getState();
if (state.machine.estopActive === true || state.machine.taskState === "estop") {
await click(doc, '[data-action="estop"]', "ensure reset estop");
await waitState(api, (next) => next.machine.estopActive === false, "ensure estop reset");
}
state = api.getState();
if (state.machine.powerOn !== true) {
await click(doc, '[data-action="power"]', "ensure power on");
await waitState(api, (next) => next.machine.powerOn === true, "ensure power on");
}
if (api.getState().machine.mode !== "manual") {
await click(doc, '[data-action="tab-manual"]', "ensure manual before home");
await waitState(api, (next) => next.machine.mode === "manual", "ensure manual before home");
}
state = api.getState();
if (state.machine.allHomed !== true) {
await click(doc, '[data-action="home-all"]', "ensure home all");
await waitState(api, (next) => next.machine.allHomed === true, "ensure home all");
}
if (api.getState().machine.mode !== "manual") {
await click(doc, '[data-action="tab-manual"]', "ensure manual");
await waitState(api, (next) => next.machine.mode === "manual", "ensure manual mode");
}
if (api.getState().runState === "running" || api.getState().runState === "paused" || api.getState().runState === "stepping") {
await click(doc, '[data-action="stop"]', "ensure stopped");
await waitState(api, (next) => next.runState === "stopped" || next.runState === "idle", "ensure stopped");
await click(doc, '[data-action="tab-manual"]', "manual after stop");
}
}
async function click(doc, selector, label) {
const element = await waitFor(() => doc.querySelector(selector), label);
element.click();
await wait(80);
return element;
}
function waitState(api, predicate, label) {
return waitFor(() => {
const state = api.getState();
return predicate(state) ? state : null;
}, label, () => summarizeState(api.getState()));
}
async function waitFor(predicate, label, describe = null) {
for (let attempt = 0; attempt < 300; attempt += 1) {
const value = predicate();
if (value) return value;
await wait(50);
}
const suffix = describe ? ` ${describe()}` : "";
throw new Error(`timed out waiting for ${label}${suffix}`);
}
function summarizeState(state) {
return JSON.stringify({
runState: state.runState,
mode: state.machine?.mode,
taskState: state.machine?.taskState,
powerOn: state.machine?.powerOn,
allHomed: state.machine?.allHomed,
kinsType: state.kinsType,
mdiHistory: state.mdiHistory?.slice?.(0, 3),
operatorMessage: state.operatorMessage,
});
}
runAllButtons().catch((error) => {
result.textContent = `xyzbc_trt_all_buttons=fail ${error.stack || error.message || error}`;
});
</script>
</body>
</html>

View File

@@ -29,7 +29,7 @@ store.dispatch({
content: [
"G90 G17",
"G0 X0 Y0 Z0",
"G1 X4 Y5 A6 C7 F100",
"G1 X4 Y5 B6 C7 F100",
"M2",
].join("\n"),
});
@@ -39,7 +39,7 @@ store.dispatch({ type: "STEP" });
const beforeSave = store.getState();
const payload = createFiveAxisSessionPayload(beforeSave);
assert.equal(payload.apiName, "web-rtcp-5axis-session-payload");
assert.equal(payload.machineProfile, "xyzac-trt");
assert.equal(payload.machineProfile, "xyzbc-trt");
assert.equal(payload.programExecution.sourceMode, "linuxcnc-interpreter-wasm");
assert.equal(payload.rtcpState, "on");
assert.equal(payload.programRuntimeFeedback.sourceMode, "linuxcnc-tp-runtime-sample");
@@ -48,7 +48,7 @@ assert.equal(payload.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_c
const storage = createMemorySessionStorage();
const saved = await store.saveSession({ storage });
assert.equal(saved.snapshot.format, "web-rtcp-5axis-session-snapshot");
assert.equal(saved.path, "web-rtcp-5axis-sim-plan/sessions/gmoccapy-web-session/web-rtcp-5axis-session.json");
assert.equal(saved.path, "web-rtcp-5axis-xyzbc-trt-sim-plan/sessions/xyzbc-trt-web-session/web-rtcp-5axis-session.json");
assert.equal(saved.storageMode, "memory");
assert.equal(store.getState().sessionPersistence.storageMode, "memory");

View File

@@ -55,7 +55,7 @@ store.dispatch({ type: "ESTOP" });
state = store.getState();
assert.equal(sidebarEntry(state, "estop").status, "emergency");
assert.equal(sidebarEntry(state, "power").allowed, false);
assert.equal(sidebarEntry(state, "power").operatorMessage, "power on blocked: reset estop first");
assert.equal(sidebarEntry(state, "power").operatorMessage, "power blocked: reset ESTOP first");
store.dispatch({ type: "RESET" });
store.dispatch({ type: "TOGGLE_POWER" });
@@ -126,7 +126,7 @@ store.dispatch({
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
state = store.getState();
assert.equal(state.machineProject.profileId, "xyzac-trt");
assert.equal(state.machineProject.projectRoot, "web-rtcp-5axis-sim-plan/machines/xyzac-trt");
assert.equal(state.machineProject.projectRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan/machines/xyzac-trt");
assert.equal(state.machineProject.ini.filename, "xyzac-trt.ini");
assert.equal(state.machineProject.ini.sourceMatchesProfile, true);
assert.equal(state.machineProject.ini.textMatchesLoadedIni, true);

View File

@@ -59,7 +59,7 @@ let state = store.getState();
let matrix = state.linuxCncParityMatrix;
assert.equal(matrix.apiName, "web-rtcp-5axis-linuxcnc-parity-matrix");
assert.equal(matrix.profileId, "xyzac-trt");
assert.equal(matrix.profileId, "xyzbc-trt");
assert.equal(matrix.status, "implemented");
assert.equal(matrix.implementedCount, matrix.itemCount);
assert.equal(matrix.rightSidebarComplete, true);
@@ -73,11 +73,11 @@ assert.equal(matrix.linuxCncFunctions.includes("M428 TCP"), true);
assert.equal(matrix.linuxCncFunctions.includes("M429 identity"), true);
assert.equal(matrix.linuxCncFunctions.includes("M430 userk"), true);
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, true);
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, false);
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").sourceMapped, true);
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").linuxCncPaths.some((path) => path.endsWith("xyzac-trt.ini")), true);
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").implemented, true);
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, false);
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, true);
assert.equal(item(matrix, "gmoccapy-trt-config").linuxCncPaths.some((path) => path.includes("gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini")), true);
assert.equal(item(matrix, "gmoccapy-trt-config").functions.includes("DISPLAY gmoccapy"), true);
assert.equal(item(matrix, "gmoccapy-native-operator-ui").active, true);

View File

@@ -102,7 +102,8 @@ assert.equal(Number.isFinite(homePose.z), true);
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 0.5 });
await waitForTaskHal(store);
state = store.getState();
assert.equal(state.taskHalStatus.motionStatus.motion.teleopMode, 1);
assert.equal(state.taskHalStatus.emcStatus.motion.valid, true);
assert.equal(state.taskHalStatus.ui.taskMode, "manual");
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
assertNear(state.axisPose.x, homePose.x + 0.5, "task/HAL JOG X+ should keep HOME work-pose continuity");
assertNear(state.axisPose.y, homePose.y, "task/HAL JOG X+ should not reset Y");
@@ -135,7 +136,8 @@ await waitForTaskHal(store);
state = store.getState();
assert.equal(state.runState, "stopped");
assert.equal(state.machine.interpState, "idle");
assert.equal(state.taskHalStatus.motionStatus.motion.aborted, true);
assert.equal(state.taskHalStatus.emcStatus.motion.valid, true);
assert.equal(state.taskHalStatus.ui.interpState, "idle");
console.log("linuxcnc_task_hal_runtime_smoke=ok");
console.log("task_hal_machine_file_smoke=ok");

View File

@@ -49,7 +49,7 @@ assert.equal(staged.save.summary.kinds.remap >= 3, true);
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "impeller-7bl-xyzac.ngc"), true);
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "boat-xyzac.ngc"), true);
assert.equal(staged.save.gcodeFiles.some((file) => file.sourceRel.endsWith("remap_subs/430remap.ngc")), true);
assert.equal(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-sim-plan/machines/xyzac-trt/")), true);
assert.equal(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-xyzbc-trt-sim-plan/machines/xyzac-trt/")), true);
assert.throws(
() => selectMachineFileProgram(staged.plan, staged.save, "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc"),
/must come from LinuxCNC source demos/,
@@ -59,6 +59,7 @@ const iniOpfsPath = staged.save.files.find((file) => file.sourceRel.endsWith("xy
assert.equal(storage.files.get(iniOpfsPath).includes("KINEMATICS = xyzac-trt-kins"), true);
const store = createSimulationStore();
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
store.dispatch({
type: "ATTACH_INTERPRETER_RUNTIME",
runtime: await createLinuxCncInterpreterRuntime(),

View File

@@ -502,20 +502,33 @@ console.log("rtcp_store_smoke=ok");
function taskHalStatusForSwitchkinsLine({ activeLine, switchkinsType }) {
return {
semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime",
task: {
state: "ON",
mode: "AUTO",
interpState: "READING",
execState: "WAITING_FOR_MOTION",
},
motionStatus: {
statusSource: "StandaloneEmcStatus",
schemaVersion: 1,
emcStatus: {
source: "StandaloneEmcStatus",
task: {
state: "ON",
mode: "AUTO",
interpState: "READING",
execState: "WAITING_FOR_MOTION",
programOpen: true,
openedLineCount: 999,
openedSourceLineCount: 999,
nextProgramLine: activeLine,
},
motion: {
valid: true,
programLine: activeLine,
motionType: 1,
switchkinsType,
currentVel: 1,
requestedVel: 1,
inPosition: false,
axisByName: { x: 1, y: 2, z: 3, a: 10, b: 0, c: 20 },
traj: {
queue: 1,
actualPosition: { x: 1, y: 2, z: 3, a: 10, b: 0, c: 20 },
},
},
},
ui: {

View File

@@ -102,7 +102,7 @@ async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
assert.equal(history.every((entry) => entry.sourceMode === "linuxcnc-task-motion-hal-wasm"), true);
assert.equal(history.every((entry) => entry.semanticBoundary === "linuxcnc_task_motion_hal_wasm_simulation_runtime"), true);
assert.equal(history.some((entry) => entry.sourceMode === "fixture-line-playback"), false);
assert.equal(history.every((entry) => entry.activeLineSource === "motion-status"), true);
assert.equal(history.every((entry) => entry.activeLineSource === "emc-status"), true);
assert.equal(history.every((entry) => entry.activeLineHalSynced === true), true);
assert.equal(history.every((entry) => entry.line === entry.motionProgramLine), true);
assert.equal(history.every((entry) => entry.line === entry.halProgramLine), true);
@@ -128,8 +128,8 @@ async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
assert.equal(state.taskHalStatus.summary.halSyncReady, true);
const activeLineBeforePause = state.activeLine;
const motionLineBeforePause = state.taskHalStatus.motionStatus.motion.programLine;
const motionAxisBeforePause = { ...state.taskHalStatus.motionStatus.axis };
const motionLineBeforePause = state.taskHalStatus.emcStatus.motion.programLine;
const motionAxisBeforePause = { ...state.taskHalStatus.ui.axisPose };
store.dispatch({ type: "PAUSE_RESUME" });
await waitForTaskHalCommand(store);
state = store.getState();
@@ -137,24 +137,24 @@ async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
assert.equal(state.machine.motionPaused, true);
assert.equal(state.machine.taskPaused, true);
assert.equal(state.taskHalStatus.ui.motionPaused, true);
assert.equal(state.taskHalStatus.motionStatus.motion.paused, true);
assert.equal(state.taskHalStatus.emcStatus.motion.paused, true);
assert.equal(state.programRuntimeFeedback.paused, true);
assert.equal(state.activeLine, activeLineBeforePause);
assert.equal(state.taskHalStatus.motionStatus.motion.programLine, motionLineBeforePause);
assert.deepEqual(state.taskHalStatus.motionStatus.axis, motionAxisBeforePause);
assert.equal(state.taskHalStatus.emcStatus.motion.programLine, motionLineBeforePause);
assert.deepEqual(state.taskHalStatus.ui.axisPose, motionAxisBeforePause);
await new Promise((resolve) => setTimeout(resolve, 120));
state = store.getState();
assert.equal(state.runState, "paused");
assert.equal(state.activeLine, activeLineBeforePause);
assert.equal(state.taskHalStatus.motionStatus.motion.programLine, motionLineBeforePause);
assert.deepEqual(state.taskHalStatus.motionStatus.axis, motionAxisBeforePause);
assert.equal(state.taskHalStatus.emcStatus.motion.programLine, motionLineBeforePause);
assert.deepEqual(state.taskHalStatus.ui.axisPose, motionAxisBeforePause);
store.dispatch({ type: "PAUSE_RESUME" });
await waitForTaskHalCommand(store);
await waitForState(store, (nextState) => nextState.machine.motionPaused === false && nextState.runState !== "paused");
state = store.getState();
assert.equal(state.machine.motionPaused, false);
assert.equal(state.taskHalStatus.motionStatus.motion.paused, false);
assert.equal(state.taskHalStatus.emcStatus.motion.paused, false);
const cycleAfterRun = state.taskHalStatus.ui.taskCycle;
store.dispatch({ type: "STEP" });

View File

@@ -119,6 +119,7 @@ assert.equal(fallbackSave.storageMode, "memory-fallback");
assert.equal(fallbackSave.storageCapability.opfsUnavailable, true);
const store = createSimulationStore();
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
const machineStorage = createMemorySessionStorage();
await store.stageMachineFiles({ storage: machineStorage });
assert.equal(store.getState().toolDbReadiness.toolDbProcessReady, true);

View File

@@ -12,6 +12,7 @@ import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc
import {
buildTaskHalProgramMotionPlan,
buildTaskHalSessionFromMachineFiles,
normalizeTaskHalStatus,
wrapTaskHalSdk,
} from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
@@ -199,9 +200,116 @@ const taskHalStatus = taskHal.readStatus();
assert.equal(taskHalStatus.summary.taskRuntimeReady, true);
assert.equal(taskHalStatus.summary.halSyncReady, true);
assert.equal(taskHalStatus.ui.activeLine >= 1, true);
assert.equal(Number.isFinite(taskHalStatus.motionStatus.axis.x), true);
assert.equal(Number.isFinite(taskHalStatus.motionStatus.axis.y), true);
assert.equal(Number.isFinite(taskHalStatus.motionStatus.axis.z), true);
assert.equal(taskHalStatus.statusSource, "StandaloneEmcStatus");
assert.equal(taskHalStatus.schemaVersion, 1);
assert.equal(taskHalStatus.emcStatus.source, "StandaloneEmcStatus");
assert.equal("task" in taskHalStatus, false);
assert.equal("motionStatus" in taskHalStatus, false);
assert.equal(taskHalStatus.ui.activeLine, taskHalStatus.emcStatus.task.currentLine);
assert.equal(taskHalStatus.ui.motionProgramLine, taskHalStatus.emcStatus.motion.programLine);
assert.equal(taskHalStatus.ui.motionQueueDepth, taskHalStatus.emcStatus.motion.traj.queue);
assert.equal(taskHalStatus.ui.axisPose.x, taskHalStatus.emcStatus.motion.axisByName.x);
assert.equal(Number.isFinite(taskHalStatus.ui.axisPose.x), true);
assert.equal(Number.isFinite(taskHalStatus.ui.axisPose.y), true);
assert.equal(Number.isFinite(taskHalStatus.ui.axisPose.z), true);
const emcOnlyTaskHalStatus = normalizeTaskHalStatus({
statusSource: "StandaloneEmcStatus",
schemaVersion: 1,
emcStatus: {
source: "StandaloneEmcStatus",
task: {
status: "EXEC",
state: "ON",
mode: "AUTO",
interpState: "READING",
execState: "EXEC",
cycle: 17,
programOpen: true,
planId: 9,
motionPlanLoaded: true,
allHomed: true,
currentLine: 12,
readLine: 12,
motionLine: 12,
callLevel: 1,
},
motion: {
valid: true,
status: "EXEC",
programLine: 12,
motionId: 44,
enabled: true,
commandQueueDepth: 2,
activeDepth: 1,
queueFull: false,
joint0: {
motorPosCmd: 10,
motorPosFb: 9.5,
},
traj: {
enabled: true,
inpos: false,
queue: 2,
activeQueue: 1,
queueFull: false,
id: 44,
paused: false,
singleStepping: false,
actualPosition: {
x: 10,
y: 20,
z: 30,
b: 40,
c: 50,
},
currentVel: 1.25,
},
axisByName: {
x: 10,
y: 20,
z: 30,
b: 40,
c: 50,
},
},
},
halSnapshot: {
ready: true,
pins: {
"motion.program-line": { value: 12 },
"motion.switchkins-type": { value: 1 },
},
},
});
assert.equal(emcOnlyTaskHalStatus.summary.taskRuntimeReady, true);
assert.equal(emcOnlyTaskHalStatus.summary.motionRuntimeReady, true);
assert.equal(emcOnlyTaskHalStatus.summary.halSyncReady, true);
assert.equal(emcOnlyTaskHalStatus.ui.taskMode, "auto");
assert.equal(emcOnlyTaskHalStatus.ui.interpState, "reading");
assert.equal(emcOnlyTaskHalStatus.ui.taskCycle, 17);
assert.equal(emcOnlyTaskHalStatus.ui.activeLine, 12);
assert.equal(emcOnlyTaskHalStatus.ui.motionQueueDepth, 2);
assert.equal(emcOnlyTaskHalStatus.ui.motionId, 44);
assert.equal(emcOnlyTaskHalStatus.ui.currentVelocity, 75);
assert.deepEqual(emcOnlyTaskHalStatus.ui.axisPose, {
x: 10,
y: 20,
z: 30,
a: 0,
b: 40,
c: 50,
});
assert.equal("task" in emcOnlyTaskHalStatus, false);
assert.equal("motionStatus" in emcOnlyTaskHalStatus, false);
assert.equal(emcOnlyTaskHalStatus.emcStatus.motion.joint0.motorPosFb, 9.5);
assert.throws(
() => normalizeTaskHalStatus({
task: { state: "ON", mode: "AUTO" },
motionStatus: { motion: { programLine: 1 }, axis: { x: 1 } },
}),
/removed legacy fields/,
);
const store = createSimulationStore();
const state = store.getState();
@@ -480,31 +588,65 @@ staleManualRunningStore.dispatch({
type: "TASK_HAL_STATUS_APPLIED",
operatorMessage: "simulated stale running task/HAL status",
status: {
taskRuntimeReady: true,
taskCommandsDriveMotionRuntime: true,
task: {
state: "ON",
mode: "AUTO",
interpState: "READING",
execState: "EXEC",
taskCycle: 123,
},
motionStatus: {
cycle: 25,
motionHalSyncReady: true,
statusSource: "StandaloneEmcStatus",
schemaVersion: 1,
emcStatus: {
source: "StandaloneEmcStatus",
task: {
state: "ON",
mode: "AUTO",
interpState: "READING",
execState: "EXEC",
cycle: 123,
taskCycle: 123,
programOpen: true,
openedLineCount: 999,
openedSourceLineCount: 999,
nextProgramLine: Number(lockedPauseState.activeLine || 1) + 5,
},
motion: {
valid: true,
programLine: Number(lockedPauseState.activeLine || 1) + 5,
currentVel: 12,
requestedVel: 12,
inPosition: false,
axisByName: {
x: 99,
y: 88,
z: 77,
b: 66,
c: 55,
},
traj: {
queue: 1,
actualPosition: {
x: 99,
y: 88,
z: 77,
b: 66,
c: 55,
},
},
},
axis: {
},
ui: {
taskState: "on",
taskMode: "auto",
interpState: "reading",
execState: "exec",
activeLine: Number(lockedPauseState.activeLine || 1) + 5,
motionProgramLine: Number(lockedPauseState.activeLine || 1) + 5,
activeLineSource: "emc-status",
taskCycle: 123,
servoCycle: 25,
axisPose: {
x: 99,
y: 88,
z: 77,
b: 66,
c: 55,
},
currentVelocity: 720,
},
halSnapshot: {
ready: true,
@@ -515,7 +657,7 @@ staleManualRunningStore.dispatch({
},
});
assert.equal(staleManualRunningStore.getState().runState, "running");
assert.equal(staleManualRunningStore.getState().machine.mode, "manual");
assert.equal(staleManualRunningStore.getState().machine.mode, "auto");
assert.equal(staleManualRunningStore.getState().machine.interpState, "reading");
assert.equal(staleManualRunningStore.getState().taskHalPauseLock, null);

View File

@@ -833,8 +833,8 @@ async function pathFromTaskHalExecution({ profile, staged, selectedPlan, executi
taskCycles: 1,
});
const status = taskHal.readStatus();
const motion = status.motionStatus?.motion || {};
const task = status.task || {};
const motion = status.emcStatus?.motion || {};
const task = status.emcStatus?.task || {};
const activeLine = status.ui?.activeLine ?? motion.programLine ?? 0;
const sample = normalizePathSample({
sampleIndex: samples.length,
@@ -1519,7 +1519,10 @@ function machineStateForMotion({
}
function xyzbcAxesFromTaskHalStatus(status = {}) {
const axis = status.motionStatus?.axis || {};
const axis = status.ui?.axisPose ||
status.emcStatus?.motion?.axisByName ||
status.emcStatus?.motion?.traj?.actualPosition ||
{};
const pins = status.halSnapshot?.pins || {};
return {
x: firstFiniteNumber(
@@ -1563,7 +1566,7 @@ function xyzbcAxesFromTaskHalStatus(status = {}) {
function spindleFromTaskHalStatus(status = {}) {
const pins = status.halSnapshot?.pins || {};
return firstFiniteNumber(
status.motionStatus?.motion?.spindleSpeed,
status.emcStatus?.motion?.spindleSpeed,
pins["spindle.0.speed-out"]?.value,
pins["motion.spindle-speed-out"]?.value,
0,

View File

@@ -11,7 +11,8 @@ const linuxCncSourceRoot = resolve(repoRoot, "linuxcnc");
const appUrlPath = "/web-rtcp-5axis-xyzbc-trt-sim-plan/app/index.html";
const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc";
const samplePeriodMs = 50;
const runStableBeforeFirstPauseMs = Math.max(Number(process.env.RUN_STABLE_BEFORE_FIRST_PAUSE_MS || 0), 0);
const runSegmentMs = 5000;
const pauseSegmentMs = 5000;
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
const outputDir = resolve(projectRoot, "working/screenshots", `estop-power-home-run-pause-50ms-${timestamp}`);
const chromiumExecutable = process.env.CHROMIUM || findSystemChromium();
@@ -68,7 +69,7 @@ try {
await clickAndWait(page, '[data-action="estop"]', "解除 ESTOP", (state) => (
state.machine?.estopActive === false &&
state.machine?.taskState === "estop-reset" &&
state.runState === "idle"
state.machine?.powerOn === false
));
await clickAndWait(page, '[data-action="power"]', "上电", (state) => (
state.machine?.powerOn === true &&
@@ -78,70 +79,72 @@ try {
state.machine?.allHomed === true &&
state.machine?.mode === "manual"
));
await clickAndWait(page, '[data-action="run"]', "Run", (state) => (
await clickAndWait(page, '[data-action="toggle-auto-manual"]', "自动模式", (state) => (
state.machine?.mode === "auto" &&
state.machine?.interpState === "idle"
));
await domClickAndWait(page, '[data-tool-id="btn_run"]', "Run", (state) => (
state.machine?.mode === "auto" &&
(state.runState === "running" || state.runState === "complete") &&
state.machine?.interpState === "reading" &&
state.programRuntimeFeedback
), 60000);
await holdState(page, "Run 后执行 5 秒", runSegmentMs, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
), { requirePositionChange: true });
if (runStableBeforeFirstPauseMs > 0) {
await holdState(page, `Run 后稳定运行 ${runStableBeforeFirstPauseMs}ms`, runStableBeforeFirstPauseMs, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
), { requirePositionChange: true });
}
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次暂停", (state) => (
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次暂停", (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
));
await holdState(page, "第一次暂停保持 5 秒", 5000, (state) => (
await holdState(page, "第一次暂停保持 5 秒", pauseSegmentMs, (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
), { freezePosition: true });
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次继续执行", (state) => (
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次继续执行", (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
await holdState(page, "第一次继续执行 10 秒", 10000, (state) => (
await holdState(page, "第一次继续执行 5 秒", runSegmentMs, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
), { requirePositionChange: true });
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次暂停", (state) => (
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次暂停", (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
));
await holdState(page, "第二次暂停保持 5 秒", 5000, (state) => (
await holdState(page, "第二次暂停保持 5 秒", pauseSegmentMs, (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
), { freezePosition: true });
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次继续执行", (state) => (
await domClickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次继续执行", (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
await holdState(page, "第二次继续执行 10 秒", 10000, (state) => (
await holdState(page, "第二次继续执行 5 秒", runSegmentMs, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
), { requirePositionChange: true });
clearInterval(capture.timer);
await capture.inFlight;
capture = null;
const finalState = await readStateSummary(page);
const gcodeDataChecks = assertGcodeExecutionDataReasonable(frames);
const manifest = {
apiName: "xyzbc-trt-estop-power-home-run-pause-50ms-verification",
status: "passed",
@@ -160,18 +163,21 @@ try {
outputDir,
captureMethod: "Playwright clicked AXIS DOM controls for ESTOP reset, power, Home All, Run, pause/resume, while a 50ms interval full-page screenshot loop recorded the visible web simulator.",
samplePeriodMs,
runStableBeforeFirstPauseMs,
runSegmentMs,
pauseSegmentMs,
expectedSequence: [
"解除 ESTOP",
"上电",
"Home All",
"自动模式",
"Run",
...(runStableBeforeFirstPauseMs > 0 ? [`Run 后稳定运行 ${runStableBeforeFirstPauseMs}ms`] : []),
"Run 后执行 5 秒",
"第一次暂停并保持 5 秒",
"第一次继续执行 10 秒",
"第一次继续执行 5 秒",
"第二次暂停并保持 5 秒",
"第二次继续执行 10 秒",
"第二次继续执行 5 秒",
],
gcodeDataChecks,
events,
assertions,
capturedFrameCount: frames.length,
@@ -226,12 +232,13 @@ function startFrameCapture(page) {
const startedAt = Date.now();
let index = 0;
let inFlight = Promise.resolve();
let capturing = false;
const timer = setInterval(() => {
if (capturing) return;
capturing = true;
const frameIndex = index;
index += 1;
inFlight = inFlight
.catch(() => {})
.then(async () => {
inFlight = (async () => {
const elapsedMs = Date.now() - startedAt;
const summary = await readStateSummary(page);
const frameName = `frame-${String(frameIndex).padStart(5, "0")}-t${String(elapsedMs).padStart(6, "0")}ms-${slug(summary.runState)}.png`;
@@ -253,6 +260,12 @@ function startFrameCapture(page) {
statement: summary.programUiExecution?.statement || "",
currentVelocity: summary.currentVelocity,
});
})()
.catch((error) => {
captureError = error;
})
.finally(() => {
capturing = false;
});
}, samplePeriodMs);
return { timer, get inFlight() { return inFlight; } };
@@ -281,6 +294,34 @@ async function clickAndWait(page, selector, label, predicate, timeoutMs = 45000)
return after;
}
async function domClickAndWait(page, selector, label, predicate, timeoutMs = 45000) {
const before = await readStateSummary(page);
const startedAt = Date.now();
await page.evaluate((targetSelector) => {
const target = document.querySelector(targetSelector);
if (!target) throw new Error(`missing click target ${targetSelector}`);
target.click();
}, selector);
const after = await waitForState(page, predicate, label, timeoutMs);
const event = {
label,
selector,
clickMethod: "dom-element-click",
at: new Date().toISOString(),
elapsedMs: Date.now() - startedAt,
before,
after: summarizeForEvent(after),
};
events.push(event);
assertions.push({
label,
passed: true,
checkedAt: new Date().toISOString(),
state: summarizeForEvent(after),
});
return after;
}
async function holdState(page, label, durationMs, predicate, options = {}) {
const startedAt = Date.now();
let last = null;
@@ -468,6 +509,79 @@ function hasPositionChanged(before, after) {
));
}
function assertGcodeExecutionDataReasonable(capturedFrames) {
const runningFrames = capturedFrames.filter((frame) => frame.runState === "running");
const pausedFrames = capturedFrames.filter((frame) => frame.runState === "paused");
if (runningFrames.length < 5) {
throw new Error(`G-code execution check failed: expected running frames, got ${runningFrames.length}`);
}
if (pausedFrames.length < 5) {
throw new Error(`G-code execution check failed: expected paused frames, got ${pausedFrames.length}`);
}
const framesWithSource = runningFrames.filter((frame) => (
frame.sourceFile || frame.statement || Number(frame.line || 0) > 0
));
if (framesWithSource.length === 0) {
throw new Error("G-code execution check failed: no running frame exposed source line data");
}
const sampleIndexes = runningFrames
.map((frame) => Number(frame.sampleIndex))
.filter(Number.isFinite);
const activeLines = runningFrames
.map((frame) => Number(frame.activeLine))
.filter(Number.isFinite);
const velocities = capturedFrames
.map((frame) => Number(frame.currentVelocity))
.filter(Number.isFinite);
const runningVelocities = runningFrames
.map((frame) => Number(frame.currentVelocity))
.filter(Number.isFinite);
if (sampleIndexes.length === 0 || Math.max(...sampleIndexes) <= Math.min(...sampleIndexes)) {
throw new Error("G-code execution check failed: sample index did not advance during running segments");
}
if (activeLines.length === 0 || activeLines.some((line) => line < 1)) {
throw new Error("G-code execution check failed: active G-code line values are invalid");
}
if (velocities.some((velocity) => velocity < -1e-6)) {
throw new Error("G-code execution check failed: negative velocity observed");
}
if (!runningVelocities.some((velocity) => velocity > 0)) {
throw new Error("G-code execution check failed: no positive velocity observed while running");
}
return {
apiName: "xyzbc-trt-gcode-execution-data-checks",
runningFrameCount: runningFrames.length,
pausedFrameCount: pausedFrames.length,
framesWithSourceCount: framesWithSource.length,
firstRunningFrame: summarizeFrameForDataCheck(runningFrames[0]),
lastRunningFrame: summarizeFrameForDataCheck(runningFrames[runningFrames.length - 1]),
minSampleIndex: Math.min(...sampleIndexes),
maxSampleIndex: Math.max(...sampleIndexes),
minActiveLine: Math.min(...activeLines),
maxActiveLine: Math.max(...activeLines),
maxRunningVelocity: Math.max(...runningVelocities),
sourceRel,
passed: true,
};
}
function summarizeFrameForDataCheck(frame = {}) {
return {
elapsedMs: frame.elapsedMs,
runState: frame.runState,
activeLine: frame.activeLine,
sampleIndex: frame.sampleIndex,
line: frame.line,
statement: frame.statement,
currentVelocity: frame.currentVelocity,
sourceFile: frame.sourceFile,
};
}
function summarizeForEvent(state) {
return {
runState: state?.runState,

View File

@@ -1,5 +1,295 @@
# 03-推进台账
## 2026-07-08 17:26 EDT - 全功能与全按钮测试
### 本轮目标
按用户要求“全面完整执行测试项目功能,每个按钮都要全面测试”,在原有 browser smoke 的基础上补齐可重复执行的全按钮自动化验证,并重新跑完整功能测试链路。
### 已做事项
- 新增全按钮浏览器测试:
- `tests/browser/xyzbc_trt_all_buttons.html`:启动真实页面,读取 `api.getButtonParity()`,对 63 个 `AXIS_BUTTON_PARITY` 按钮/动作逐项确认 DOM 控件、LinuxCNC 来源元数据和点击后状态效果。
- `tests/browser/verify_xyzbc_trt_all_buttons.mjs`:使用 Playwright 和临时 HTTP server 执行真实浏览器测试。
- `tests/browser/verify_xyzbc_trt_all_buttons.sh`:命令行入口。
- `app/package.json` 新增 `smoke:buttons`。
- 全按钮测试覆盖内容:
- 菜单:Stage、Save session、Restore session、Reload、View X/Y/Z/P、Clear Live Plot、Audit。
- 工具栏:ESTOP/RESET、Power on/off、AUTO/MANUAL、Reload、Run、Pause/Resume、Step、Stop、视图和清轨迹。
- 手动区:Manual/MDI tab、joint 选择、jog increment、Jog +/−、Touch Off、Tool Touch Off。
- MDI/PyVCP:MDI submit、MDI history、M428 TCP、M429 identity、M430 userk、vismach-clear。
- Override/冷却/主轴:feed/rapid/spindle override +/−、spindle reverse/stop/forward、flood、mist、block delete、optional stop、ignore limits。
- 修复全按钮测试发现的真实状态问题:
- `app/src/state/store.js` 中 MDI M428/M429/M430 已经解析出的 `kinsType`/`rtcpState`,在随后 task/HAL 状态回写时会被旧的 runtime status 覆盖回 identity。
- 修复为 MDI task/HAL 命令回写时通过 `preserveMachine.kinsType` 保留当前 MDI switchkins 结果。
- 修正旧测试口径:
- `verify_five_axis_session.mjs` 更新为当前目标项目默认 `xyzbc-trt` 和 `xyzbc-trt-web-session`。
- `verify_gmoccapy_trt_project_sidebar.mjs` 更新 power ESTOP 文案与当前项目 OPFS 根。
- `verify_linuxcnc_parity_matrix.mjs` 更新默认 active profile 为 `xyzbc-trt`。
- `verify_machine_file_staging.mjs` 与 `verify_tool_db_web_simulation.mjs` 对仍验证 `xyzac-trt` 的场景先显式 `SET_PROFILE`,避免拿当前默认 `xyzbc-trt` 状态和旧 profile 预期比较。
### 执行命令
```text
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
for test_file in web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/*.mjs; do node "$test_file"; done
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:buttons
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:browser
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
git diff --check -- web-rtcp-5axis-xyzbc-trt-sim-plan/app/src web-rtcp-5axis-xyzbc-trt-sim-plan/app/package.json web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src web-rtcp-5axis-xyzbc-trt-sim-plan/tests web-rtcp-5axis-xyzbc-trt-sim-plan/working web-rtcp-5axis-xyzbc-trt-sim-plan/tools
```
### 关键结果
```text
gmoccapy_static_build=ok
全部 tests/node/*.mjs 通过
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
xyzbc_trt_all_buttons=ok checked=63
xyzbc_trt_browser_smoke=ok
compare_xyzbc_trt_status=pass
git diff --check 无输出
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T21:25:01.630Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T21:25:02.164Z
compare.surfaceSummary.passCount=60
compare.surfaceSummary.failCount=0
compare.functionalSummary.status=pass
compare.functionalSummary.passCount=8
compare.functionalSummary.failCount=0
compare.requiredImprovements=[]
```
### 结论
T-084 完成。目标项目已新增可重复的 63 项全按钮浏览器测试,并完成完整功能测试链路。全按钮测试暴露并修复了 MDI switchkins 后 `kinsType` 被 task/HAL 状态回写覆盖的问题;全量 Node、WASM、browser、buttons、evidence/compare 均通过。
## 2026-07-08 10:42 EDT - 目标项目删除顶层旧字段
### 本轮目标
按用户要求“`wasm-port` 中已经删除旧字段,目标项目也要删除旧字段,使用新方法”,将 Web 项目从“拒绝旧输入但仍可能生成内部顶层旧视图”进一步收敛为:正常 `taskHalStatus` 只保留新版 `emcStatus` 和面向 UI 的 `ui`,不再输出或消费顶层 `task`/`motionStatus`。
### 已做事项
- 修改 `app/src/runtime/linuxcnc-task-hal-runtime.js`:
- `normalizeTaskHalStatus()` 继续拒绝旧 `task`、`motionStatus`、`rcsStatus`、`taskTopLevelStatus`、顶层 `servoCycle`。
- 正常返回对象不再生成顶层 `task` 或 `motionStatus`,所有 task/motion 事实来自 `emcStatus.task` 和 `emcStatus.motion`。
- 修改 `app/src/state/linuxcnc-task-policy.js` 与 `app/src/state/store.js`:
- policy、状态循环、暂停/继续、jog、runtime feedback 均改为读取 `emcStatus`/`ui`。
- `TASK_HAL_STATUS_APPLIED` 入口对旧字段直接失败,不再兼容旧形态。
- 修改 `tools/collect-web-xyzbc-trt-evidence.mjs`:
- Web evidence 的执行采样、轴值和主轴状态改为读取 `emcStatus`/`ui`,不再读取 `motionStatus` 或顶层 `task`。
- 修改 Node 测试:
- 增加正常 `taskHalStatus` 中不存在 `task`/`motionStatus` 的断言。
- 将手工状态夹具改为新版 `emcStatus`/`ui` 结构。
### 执行命令
```text
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_xyzbc_trt_web_app.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_run_feedback_loop.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_rtcp_store.mjs
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
bash web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
```
### 关键结果
```text
xyzbc_trt_web_app_smoke=ok
linuxcnc_task_hal_runtime_smoke=ok
run_feedback_status_loop_smoke=ok
rtcp_store_smoke=ok
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
gmoccapy_static_build=ok
xyzbc_trt_browser_smoke=ok
compare_xyzbc_trt_status=pass
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T14:41:49.397Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T14:41:49.928Z
compare.surfaceSummary.passCount=60
compare.surfaceSummary.failCount=0
compare.functionalSummary.status=pass
compare.functionalSummary.passCount=8
compare.functionalSummary.failCount=0
compare.requiredImprovements=[]
```
### 结论
T-083 完成。目标 Web 项目已跟随新版 `wasm-port` 删除顶层旧字段:正常状态对象不再输出 `task`/`motionStatus`,程序内部也不再消费这些旧字段;旧字段仅作为无效输入被拒绝。
## 2026-07-08 10:01 EDT - 移除旧 task/motionStatus 输入兼容
### 本轮目标
按用户要求“不保留旧字段兼容,修改程序”,将上一轮状态适配从“以 `emcStatus` 为主并兼容旧输入”改为“只接受新版 `StandaloneEmcStatus`/`emcStatus` 输入”。
### 已做事项
- 修改 `app/src/runtime/linuxcnc-task-hal-runtime.js`:
- `normalizeTaskHalStatus()` 现在要求 `status.emcStatus.task` 和 `status.emcStatus.motion` 存在,缺失时直接抛错。
- 移除从旧 `status.task`、`status.motionStatus.motion`、`status.motionStatus.axis`、`status.motionStatus.joint0` 的回退读取。
- 本轮完成后继续在 T-083 删除 Web 顶层 `task`/`motionStatus` 派生输出,当前正常状态只保留 `emcStatus` 和 `ui`。
- `activeLineSource` 改为 `emc-status`。
- 修改测试:
- `verify_xyzbc_trt_web_app.mjs` 增加旧 `task`/`motionStatus` 输入拒绝断言。
- `verify_linuxcnc_task_hal_runtime.mjs` 不再断言旧 `teleopMode`/`aborted` 字段,改为断言新版 `emcStatus` 有效性和可观察运行结果。
- `verify_run_feedback_loop.mjs` 将 active line 来源断言更新为 `emc-status`。
- 重新构建静态产物并刷新 Web evidence/compare。
### 执行命令
```text
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_xyzbc_trt_web_app.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_run_feedback_loop.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_rtcp_store.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
bash web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
```
### 关键结果
```text
xyzbc_trt_web_app_smoke=ok
linuxcnc_task_hal_runtime_smoke=ok
run_feedback_status_loop_smoke=ok
rtcp_store_smoke=ok
gmoccapy_static_build=ok
xyzbc_trt_browser_smoke=ok
legacy_task_motion_status_rejected=ok
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
compare_xyzbc_trt_status=pass
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T14:00:44.425Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T14:00:58.972Z
compare.surfaceSummary.passCount=60
compare.surfaceSummary.failCount=0
compare.functionalSummary.status=pass
compare.functionalSummary.passCount=8
compare.functionalSummary.failCount=0
compare.requiredImprovements=[]
```
### 结论
旧 `task`/`motionStatus` 输入兼容已移除,T-082 完成。当前 Web task/HAL runtime 只接受新版 `StandaloneEmcStatus`/`emcStatus` 合同;后续 T-083 已进一步删除 Web 顶层 `task`/`motionStatus` 输出。
## 2026-07-08 09:50 EDT - 新版 WASM 状态合同适配收口
### 本轮目标
继续完成新版 `/home/mes123456/cnc_wams/wasm-port` 完善后目标项目剩余适配工作:将 task/HAL status 主状态来源收敛到新版 `StandaloneEmcStatus`/`emcStatus`,并刷新 Web evidence/compare 确认无新增缺口。
### 已做事项
- 复核 `working/README.md`、`04-任务矩阵.md`、`05-验收证据.md` 和当前 evidence 摘要,确认旧任务 T-001 到 T-080 已完成,本轮需要把新版 WASM 状态合同适配作为新增闭环任务记录。
- 完成 `app/src/runtime/linuxcnc-task-hal-runtime.js` 适配:以 `emcStatus.task`、`emcStatus.motion`、`emcStatus.motion.traj`、`emcStatus.motion.axisByName`、`emcStatus.motion.joint0` 作为唯一输入生成 Web UI 消费字段,旧 `task`/`motionStatus` 输入会被拒绝。
- 在 `tests/node/verify_xyzbc_trt_web_app.mjs` 增加真实新版 status contract 断言和纯 `emcStatus` 输入归一化断言。
- 重新构建静态产物,同步 `app/dist/src/runtime/linuxcnc-task-hal-runtime.js` 与 `app/dist/wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js/.wasm`。
- 刷新 Web evidence 和 compare evidence。
### 执行命令
```text
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_xyzbc_trt_web_app.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_run_feedback_loop.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
bash web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
```
### 关键结果
```text
xyzbc_trt_web_app_smoke=ok
linuxcnc_task_hal_runtime_smoke=ok
run_feedback_status_loop_smoke=ok
gmoccapy_static_build=ok
xyzbc_trt_browser_smoke=ok
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
compare_xyzbc_trt_status=pass
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T13:50:12.039Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T13:50:23.202Z
compare.surfaceSummary.passCount=60
compare.surfaceSummary.failCount=0
compare.functionalSummary.status=pass
compare.functionalSummary.passCount=8
compare.functionalSummary.failCount=0
compare.requiredImprovements=[]
```
### 结论
新版 WASM 状态合同适配已闭合,T-081 完成。当前目标项目对新版 `StandaloneEmcStatus`/`emcStatus` 可直接消费,且不再接受旧 `task`/`motionStatus` 输入;刷新后的 Web evidence 和 compare 没有新增 fail、blocker 或 required improvement。
## 2026-07-07 19:03 EDT - working 全部工作收口复验
### 本轮目标

View File

@@ -82,6 +82,10 @@
| T-078 | 2026-07-07 AXIS 主控制按钮状态链路收敛 | 完成 | 按 `18-20260707-AXIS主控制按钮调用链与Web完善指南.md` 完成 ESTOP、Power、Home、Run、Pause/Resume、Step 的 LinuxCNC task/motion/home 状态收敛;Task/HAL WASM 暴露 `motionEnabled`、`homed[]`、`singleStepping`、`motionStepping`;policy 统一门禁;build、Node/WASM/browser smoke、Web evidence 和 compare 均通过,最新 `compare.status=pass`、`60/60 pass`、`blockers=[]`、`requiredImprovements=[]` |
| T-079 | 2026-07-07 AXIS 按钮 LinuxCNC C++ 功能级 compare 硬验收 | 完成 | 按 `20-20260707-AXIS按钮LinuxCNC真实C++对标实施步骤.md` 和 `21-20260707-AXIS按钮LinuxCNC真实C++对标验收测试.md` 增强 `tools/compare-xyzbc-trt-evidence.mjs`;compare 输出 `surfaceSummary` 与 `functionalSummary`,并硬检查 `cppTaskStateMachineParity`、`cppModeGateParity`、`cppHomingParity`、`cppRunGateParity`、`cppPauseResumeParity`、`cppStepParity`、`illegalCommandParity`、`realProgramExecutionParity`;最新 `surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]` |
| T-080 | 2026-07-07 19:03 EDT working 全部工作收口复验 | 完成 | 重新执行 native LinuxCNC evidence、Web evidence、compare、Node smoke、build、Task/HAL 状态矩阵、Task/HAL WASM/SDK smoke、browser smoke;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]` |
| T-081 | 2026-07-08 新版 WASM `StandaloneEmcStatus` 状态合同适配 | 完成 | Web task/HAL runtime 以新版 `emcStatus` 为唯一输入状态来源,旧 `task`/`motionStatus` 输入会被拒绝;目标 Node smoke、WASM `verify_task_status_json_contract.mjs`、Task/HAL SDK smoke、build、browser smoke、Web evidence 和 compare 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]` |
| T-082 | 2026-07-08 移除旧 `task`/`motionStatus` 输入兼容 | 完成 | `normalizeTaskHalStatus()` 缺少 `StandaloneEmcStatus`/`emcStatus.task`/`emcStatus.motion` 时直接失败,不再读取旧 `status.task` 或 `status.motionStatus`;旧格式拒绝检查、目标 Node smoke、build、browser smoke、Web evidence 和 compare 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]` |
| T-083 | 2026-07-08 目标项目删除顶层旧字段 | 完成 | Web 正常 `taskHalStatus` 不再输出或消费顶层 `task`/`motionStatus`;runtime、store、policy、feedback、Web evidence 统一读取 `emcStatus`/`ui`;旧字段只作为无效输入被拒绝;目标 Node smoke、WASM status JSON contract、Task/HAL SDK、build、browser smoke、Web evidence 和 compare 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]` |
| T-084 | 2026-07-08 全功能与全按钮测试 | 完成 | 新增 `npm run smoke:buttons`,真实浏览器逐项测试 `AXIS_BUTTON_PARITY` 63 个按钮/动作;修复 M428/M429/M430 后 task/HAL 状态回写覆盖 `kinsType` 的问题;完整 `tests/node/*.mjs`、WASM status contract、Task/HAL SDK、build、browser smoke、全按钮 smoke、Web evidence 和 compare 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]` |
状态说明:
@@ -89,7 +93,7 @@
- 待前置:代码已接入,但仍依赖未完成的外部或运行环境步骤。
- 待实现:已记录验收要求,仍需修改采集或对比脚本。
当前 T-001 到 T-080 均已完成;T-047/T-048/T-049/T-050 是用户指出 `working/screenshots/web-simulation-full-process-20260703T051258Z` 截图问题后的页面/截图层真实执行闭环项。
当前 T-001 到 T-084 均已完成;T-047/T-048/T-049/T-050 是用户指出 `working/screenshots/web-simulation-full-process-20260703T051258Z` 截图问题后的页面/截图层真实执行闭环项。
T-051 到 T-075 是 2026-07-04 新增的严格完整对标任务:以 `/home/mes123456/cnc_wams/linuxcnc` 源树和 LinuxCNC `xyzbc-trt` 真实执行为唯一 native 权威基线,当前已完成,最新 compare 为 60/60 pass,blockers=[]。
@@ -102,3 +106,11 @@ T-078 是 2026-07-07 按 AXIS 主控制按钮调用链完善指南完成的状
T-079 是 2026-07-07 按 20/21 号文档完成的功能级硬验收任务:旧 `summary/checks` 保持 60 项表面兼容检查,新增 `functionalChecks/functionalSummary` 作为真实通过判定;最新 compare 为 `surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`。
T-080 是 2026-07-07 19:03 EDT 针对用户“完成 working 中全部工作”的收口复验任务:重新采集 native/Web evidence 并重跑 compare、smoke、build、Task/HAL 状态矩阵和浏览器验证,确认所有 working 任务仍处于完成状态。
T-081 是 2026-07-08 针对新版 `wasm-port` task/HAL WASM 完善后的适配任务:目标项目 runtime 以 `StandaloneEmcStatus`/`emcStatus` 为主合同,测试覆盖纯 `emcStatus` 输入和真实 WASM status,刷新后的 Web evidence/compare 仍为通过。
T-082 是 2026-07-08 按用户要求移除旧字段兼容的任务:目标项目 runtime 不再接受旧 `task`/`motionStatus` 输入。
T-083 是 2026-07-08 按用户要求跟随 `wasm-port` 删除旧字段的任务:目标项目正常状态对象不再输出顶层 `task`/`motionStatus`,内部代码和 evidence 采集改为使用 `emcStatus`/`ui`。
T-084 是 2026-07-08 按用户要求全面测试功能和每个按钮的任务:新增 63 项全按钮浏览器测试入口,修复 MDI switchkins 后 task/HAL 状态回写覆盖问题,并完成全量 Node/WASM/browser/evidence/compare 验收。

View File

@@ -1,5 +1,339 @@
# 05-验收证据
## 2026-07-08 17:26 EDT - 全功能与全按钮测试验收证据
本轮按用户要求全面执行目标项目功能测试,并补齐“每个按钮都要全面测试”的自动化入口。新增全按钮浏览器测试覆盖 `AXIS_BUTTON_PARITY` 63 个按钮/动作条目,逐项验证 DOM 控件、LinuxCNC 来源元数据和点击后的状态效果。
关键实现证据:
```text
app/package.json
app/src/state/store.js
tests/browser/xyzbc_trt_all_buttons.html
tests/browser/verify_xyzbc_trt_all_buttons.mjs
tests/browser/verify_xyzbc_trt_all_buttons.sh
tests/node/verify_five_axis_session.mjs
tests/node/verify_gmoccapy_trt_project_sidebar.mjs
tests/node/verify_linuxcnc_parity_matrix.mjs
tests/node/verify_machine_file_staging.mjs
tests/node/verify_tool_db_web_simulation.mjs
working/evidence/web-xyzbc-trt-evidence.json
working/evidence/compare-xyzbc-trt-evidence.json
```
执行命令:
```text
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
for test_file in web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/*.mjs; do node "$test_file"; done
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:buttons
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:browser
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
git diff --check -- web-rtcp-5axis-xyzbc-trt-sim-plan/app/src web-rtcp-5axis-xyzbc-trt-sim-plan/app/package.json web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/src web-rtcp-5axis-xyzbc-trt-sim-plan/tests web-rtcp-5axis-xyzbc-trt-sim-plan/working web-rtcp-5axis-xyzbc-trt-sim-plan/tools
```
关键输出:
```text
gmoccapy_static_build=ok
five_axis_session_smoke=ok
full_execution_boundary_smoke=ok
full_linuxcnc_5axis_source_node_smoke=ok
gmoccapy_communication_model_smoke=ok
gmoccapy_hal_model_smoke=ok
gmoccapy_icon_manifest_smoke=ok
gmoccapy_icon_registry_smoke=ok
gmoccapy_trt_project_sidebar_smoke=ok
gmoccapy_xyzab_gates_smoke=ok
gmoccapy_xyzab_profile_smoke=ok
gmoccapy_xyzac_trt_parity_smoke=ok
impeller_feed_task_hal_run=ok
linear_unit_conversion_smoke=ok
linuxcnc_ini_runtime_smoke=ok
linuxcnc_interpreter_runtime_smoke=ok
linuxcnc_kinematics_runtime_smoke=ok
linuxcnc_parity_matrix_smoke=ok
linuxcnc_task_hal_runtime_smoke=ok
machine_file_staging_smoke=ok
native_task_hal_source_artifact_audit=ok
profile_boundary_smoke=ok
real_linuxcnc_5axis_program_cases_smoke=ok
rtcp_store_smoke=ok
run_feedback_status_loop_smoke=ok
run_preconditions_machine_file_smoke=ok
tool_db_user_m_simulation_smoke=ok
tool_db_web_simulation_smoke=ok
xyzbc_trt_web_app_smoke=ok
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
xyzbc_trt_all_buttons=ok checked=63
xyzbc_trt_browser_smoke=ok
compare_xyzbc_trt_status=pass
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T21:25:01.630Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T21:25:02.164Z
surfaceSummary.checkCount=60
surfaceSummary.passCount=60
surfaceSummary.failCount=0
functionalSummary.status=pass
functionalSummary.checkCount=8
functionalSummary.passCount=8
functionalSummary.failCount=0
functionalSummary.requiredImprovements=[]
compare.requiredImprovements=[]
```
结论:全功能和全按钮测试已完成。新增 `smoke:buttons` 覆盖 63 个按钮/动作;本轮发现并修复 M428/M429/M430 MDI 后 `kinsType` 被 task/HAL 状态回写覆盖的问题。当前完整 Node、WASM、browser、buttons、evidence/compare 验收均通过。
## 2026-07-08 10:42 EDT - 目标项目删除顶层旧字段验收证据
本轮按用户要求让目标 Web 项目跟随 `/home/mes123456/cnc_wams/wasm-port` 删除旧字段。当前正常 `taskHalStatus` 不再输出或消费顶层 `task`/`motionStatus`,runtime、store、policy、feedback 和 Web evidence 均使用 `emcStatus`/`ui`。
关键实现证据:
```text
app/src/runtime/linuxcnc-task-hal-runtime.js
app/src/state/linuxcnc-task-policy.js
app/src/state/store.js
tools/collect-web-xyzbc-trt-evidence.mjs
tests/node/verify_xyzbc_trt_web_app.mjs
tests/node/verify_run_feedback_loop.mjs
tests/node/verify_rtcp_store.mjs
app/dist/src/runtime/linuxcnc-task-hal-runtime.js
working/evidence/web-xyzbc-trt-evidence.json
working/evidence/compare-xyzbc-trt-evidence.json
```
执行命令:
```text
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_xyzbc_trt_web_app.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_run_feedback_loop.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_rtcp_store.mjs
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
bash web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
```
关键输出:
```text
xyzbc_trt_web_app_smoke=ok
linuxcnc_task_hal_runtime_smoke=ok
task_hal_machine_file_smoke=ok
switchkins_remap_hal_sync_smoke=ok
browser_task_hal_worker_smoke=ok
run_feedback_status_loop_smoke=ok
run_ready_sequence_smoke=ok
pause_uses_motion_paused_gate=ok
pause_freezes_task_hal_status_loop=ok
rtcp_store_smoke=ok
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
gmoccapy_static_build=ok
xyzbc_trt_browser_smoke=ok
compare_xyzbc_trt_status=pass
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T14:41:49.397Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T14:41:49.928Z
surfaceSummary.checkCount=60
surfaceSummary.passCount=60
surfaceSummary.failCount=0
functionalSummary.status=pass
functionalSummary.checkCount=8
functionalSummary.passCount=8
functionalSummary.failCount=0
functionalSummary.requiredImprovements=[]
compare.requiredImprovements=[]
```
结论:目标 Web 项目已删除顶层旧字段。旧 `task`/`motionStatus` 只在拒绝输入的负向测试和错误提示中出现,正常运行数据以 `emcStatus`/`ui` 为准。
## 2026-07-08 10:01 EDT - 移除旧字段兼容验收证据
本轮按用户要求“不保留旧字段兼容,修改程序”,将 Web task/HAL runtime 改为只接受新版 `StandaloneEmcStatus`/`emcStatus` 输入。旧 `task`/`motionStatus` 输入不再被兼容归一化。
关键实现证据:
```text
app/src/runtime/linuxcnc-task-hal-runtime.js
tests/node/verify_xyzbc_trt_web_app.mjs
tests/node/verify_linuxcnc_task_hal_runtime.mjs
tests/node/verify_run_feedback_loop.mjs
app/dist/src/runtime/linuxcnc-task-hal-runtime.js
working/evidence/web-xyzbc-trt-evidence.json
working/evidence/compare-xyzbc-trt-evidence.json
```
执行命令:
```text
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_xyzbc_trt_web_app.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_run_feedback_loop.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_rtcp_store.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
bash web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
```
关键输出:
```text
xyzbc_trt_web_app_smoke=ok
linuxcnc_task_hal_runtime_smoke=ok
task_hal_machine_file_smoke=ok
switchkins_remap_hal_sync_smoke=ok
browser_task_hal_worker_smoke=ok
run_feedback_status_loop_smoke=ok
run_ready_sequence_smoke=ok
pause_uses_motion_paused_gate=ok
pause_freezes_task_hal_status_loop=ok
rtcp_store_smoke=ok
gmoccapy_static_build=ok
xyzbc_trt_browser_smoke=ok
legacy_task_motion_status_rejected=ok
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
compare_xyzbc_trt_status=pass
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T14:00:44.425Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T14:00:58.972Z
surfaceSummary.checkCount=60
surfaceSummary.passCount=60
surfaceSummary.failCount=0
functionalSummary.status=pass
functionalSummary.checkCount=8
functionalSummary.passCount=8
functionalSummary.failCount=0
functionalSummary.requiredImprovements=[]
compare.requiredImprovements=[]
```
结论:旧字段输入兼容已移除并通过验证。后续 T-083 已进一步删除 Web 正常状态对象里的顶层 `task`/`motionStatus`,仅保留新版 `emcStatus` 与 UI 所需 `ui`。
## 2026-07-08 09:50 EDT - 新版 WASM 状态合同适配验收证据
本轮继续完成新版 `/home/mes123456/cnc_wams/wasm-port` task/HAL WASM 完善后的目标项目适配。关键目标是让 Web runtime 直接消费新版 `StandaloneEmcStatus`/`emcStatus` 状态结构,并拒绝旧 `task`/`motionStatus` 输入;后续 T-083 已删除 Web 顶层 `task`/`motionStatus` 派生视图,正常状态只保留 `emcStatus` 和 `ui`。
关键实现证据:
```text
app/src/runtime/linuxcnc-task-hal-runtime.js
tests/node/verify_xyzbc_trt_web_app.mjs
app/dist/src/runtime/linuxcnc-task-hal-runtime.js
app/dist/wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js
app/dist/wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm
working/evidence/web-xyzbc-trt-evidence.json
working/evidence/compare-xyzbc-trt-evidence.json
```
执行命令:
```text
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_xyzbc_trt_web_app.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-xyzbc-trt-sim-plan/tests/node/verify_run_feedback_loop.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
bash web-rtcp-5axis-xyzbc-trt-sim-plan/tests/browser/verify_xyzbc_trt_browser.sh
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
node wasm-port/tests/wasm/node/verify_task_status_json_contract.mjs
node wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
```
关键输出:
```text
xyzbc_trt_web_app_smoke=ok
linuxcnc_task_hal_runtime_smoke=ok
task_hal_machine_file_smoke=ok
switchkins_remap_hal_sync_smoke=ok
browser_task_hal_worker_smoke=ok
run_feedback_status_loop_smoke=ok
run_ready_sequence_smoke=ok
pause_uses_motion_paused_gate=ok
pause_freezes_task_hal_status_loop=ok
gmoccapy_static_build=ok
xyzbc_trt_browser_smoke=ok
task_status_json_contract_runtime=ok
task_status_json_contract_schema=ok
task_status_json_contract=ok
linuxcnc_task_hal_sdk=ok
task_hal_sdk_status_snapshot=ok
task_hal_sdk_standalone_emc_status=ok
task_hal_sdk_status_json_contract=ok
compare_xyzbc_trt_status=pass
```
最新 evidence 摘要:
```text
web.status=ready-for-wasm-runtime
web.collectedAt=2026-07-08T13:50:12.039Z
web.blockers=[]
web.taskHalEquivalence.ready=true
web.executionPath.status=ok
web.executionPath.sampleCount=228
compare.status=pass
compare.comparedAt=2026-07-08T13:50:23.202Z
surfaceSummary.checkCount=60
surfaceSummary.passCount=60
surfaceSummary.failCount=0
functionalSummary.status=pass
functionalSummary.checkCount=8
functionalSummary.passCount=8
functionalSummary.failCount=0
functionalSummary.requiredImprovements=[]
compare.requiredImprovements=[]
```
结论:新版 WASM 状态合同适配完成。目标项目可以直接从 `emcStatus.task`、`emcStatus.motion`、`emcStatus.motion.traj`、`axisByName`、`joint0` 生成 Web UI 状态,并且刷新后的 compare 仍为表面检查 60/60、功能级检查 8/8 通过。
## 2026-07-07 19:03 EDT - working 全部工作收口复验证据
本轮按用户要求完成 `working` 中全部工作,并重新执行收口复验。

View File

@@ -22,6 +22,10 @@
当前新增关注点:
- 2026-07-08 17:26 EDT 已按用户要求全面执行目标项目功能测试,并新增 `npm run smoke:buttons` 全按钮浏览器测试入口:覆盖 `AXIS_BUTTON_PARITY` 的 63 个按钮/动作条目,逐项检查 DOM 控件、LinuxCNC 来源元数据和点击后的状态效果;本轮同时修复 M428/M429/M430 MDI 后 task/HAL 状态回写覆盖 `kinsType` 的问题,并修正旧默认机型/旧项目根测试口径。完整 Node 测试集、WASM status contract、Task/HAL SDK、build、browser smoke、全按钮 smoke、Web evidence 和 compare 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]`。
- 2026-07-08 10:42 EDT 已按用户要求在目标 Web 项目中删除顶层旧字段:`taskHalStatus` 正常形态不再输出或消费 `task`/`motionStatus`,store、policy、feedback、evidence 采集统一读取 `emcStatus` 和 `ui`;旧字段只保留在拒绝输入的负向测试和错误提示中。Node smoke、WASM status JSON contract、Task/HAL SDK、build、browser smoke、Web evidence 和 compare 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]`。
- 2026-07-08 10:01 EDT 已按用户要求移除旧字段兼容:`normalizeTaskHalStatus()` 现在缺少 `StandaloneEmcStatus`/`emcStatus.task`/`emcStatus.motion` 时直接失败,不再从旧 `status.task` 或 `status.motionStatus` 回退;旧格式拒绝检查、Node smoke、browser smoke、WASM status JSON contract、Task/HAL SDK、Web evidence 和 compare 均通过。
- 2026-07-08 09:50 EDT 已完成新版 `wasm-port` task/HAL WASM 状态合同适配:Web runtime 以 `StandaloneEmcStatus`/`emcStatus` 为唯一输入合同,拒绝旧 `task`/`motionStatus` 输入字段;目标 Node smoke、WASM status JSON contract、SDK smoke、Web evidence 和 compare 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]`。
- 2026-07-07 19:03 EDT 已按“完成 working 中全部工作”重新执行当前收口复验:native LinuxCNC evidence、Web evidence、compare、Node smoke、build、Task/HAL 状态矩阵、Task/HAL WASM/SDK smoke、browser smoke 均通过;最新 `compare.status=pass`、`surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]`。
- 2026-07-07 18:57 EDT 已完成 20/21 号文档要求的功能级 compare 增强:`compare-xyzbc-trt-evidence.json` 现在同时输出 `surfaceSummary` 和 `functionalSummary`;最新结果为 `surfaceSummary=60/60 pass`、`functionalSummary=8/8 pass`、`requiredImprovements=[]`,硬规则覆盖 `cppTaskStateMachineParity`、`cppModeGateParity`、`cppHomingParity`、`cppRunGateParity`、`cppPauseResumeParity`、`cppStepParity`、`illegalCommandParity`、`realProgramExecutionParity`。
- 2026-07-07 18:35 EDT 已将 19 号源码分析拆分为 20 号实施步骤和 21 号验收测试。后续结论必须区分 `surfaceSummary` 和 `functionalSummary`;`compare 60/60 pass` 只代表表面规则通过,真实通过必须证明 Web 行为符合 LinuxCNC C++ `emctaskmain.cc`、`emctask.cc`、`taskintf.cc`、`motion/command.c`、`control.c`、`homing.c` 的状态机和真实程序执行结果。

View File

@@ -1,7 +1,7 @@
{
"apiName": "xyzbc-trt-native-web-evidence-comparison",
"status": "pass",
"comparedAt": "2026-07-07T23:03:43.547Z",
"comparedAt": "2026-07-08T21:25:02.164Z",
"nativePath": "/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/native-xyzbc-trt-evidence.json",
"webPath": "/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json",
"summary": {
@@ -567,7 +567,7 @@
"execution": {
"completed": true,
"sampleCount": 228,
"taskHalEventCount": 30
"taskHalEventCount": 35
},
"semanticBoundary": "web_basic_sim_joint_home_spindle_manualtoolchange_feedback"
},
@@ -578,7 +578,7 @@
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"sessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt/demos/xyzbc_switchkins.ngc",
"completed": true,
"eventCount": 30
"eventCount": 35
},
"taskPolicy": {
"taskState": "estop-reset",
@@ -619,7 +619,7 @@
"execution": {
"completed": true,
"sampleCount": 228,
"taskHalEventCount": 30
"taskHalEventCount": 35
},
"semanticBoundary": "web_basic_sim_joint_home_spindle_manualtoolchange_feedback"
},
@@ -2469,7 +2469,7 @@
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"sessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt/demos/xyzbc_switchkins.ngc",
"completed": true,
"eventCount": 30
"eventCount": 35
},
"executionPath": {
"nativeSampleCount": 4,

View File

@@ -1,7 +1,7 @@
{
"apiName": "xyzbc-trt-web-opfs-wasm-evidence",
"status": "ready-for-wasm-runtime",
"collectedAt": "2026-07-07T23:03:42.947Z",
"collectedAt": "2026-07-08T21:25:01.630Z",
"profile": {
"id": "xyzbc-trt",
"machineName": "sim-xyzbc-trt-kins (switchkins)",
@@ -102296,7 +102296,7 @@
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"sessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt/demos/xyzbc_switchkins.ngc",
"completed": true,
"eventCount": 30
"eventCount": 35
}
},
"semanticExecutionPath": {
@@ -227512,7 +227512,7 @@
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"sessionProgramPath": "/work/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt/demos/xyzbc_switchkins.ngc",
"completed": true,
"eventCount": 30
"eventCount": 35
},
"taskPolicy": {
"taskState": "estop-reset",
@@ -227553,7 +227553,7 @@
"execution": {
"completed": true,
"sampleCount": 228,
"taskHalEventCount": 30
"taskHalEventCount": 35
},
"semanticBoundary": "web_basic_sim_joint_home_spindle_manualtoolchange_feedback"
},
@@ -227584,7 +227584,7 @@
"execution": {
"completed": true,
"sampleCount": 228,
"taskHalEventCount": 30
"taskHalEventCount": 35
},
"semanticBoundary": "web_basic_sim_joint_home_spindle_manualtoolchange_feedback"
},
@@ -229820,16 +229820,16 @@
{
"rel": "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js",
"absolutePath": "/home/mes123456/cnc_wams/wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js",
"bytes": 19878,
"mtime": "2026-07-07T22:01:17.840Z",
"sha256": "b5f5caa1a898acea8280dfba6dae7b723d775e54e9d769bdb4d21ac6ac2ac358"
"bytes": 20858,
"mtime": "2026-07-08T14:15:38.726Z",
"sha256": "31a4b38ab6c92d5524c80414a159ae874f72bcbe4d3744019cefd18bb224c8f6"
},
{
"rel": "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm",
"absolutePath": "/home/mes123456/cnc_wams/wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm",
"bytes": 215042,
"mtime": "2026-07-07T22:01:17.596Z",
"sha256": "b456ffb64e5a32bc4a425343b5539dea9cf064b1804c4c6b53dea3661ea37fa8"
"bytes": 281624,
"mtime": "2026-07-08T14:15:38.526Z",
"sha256": "ed62ea4a6138c3bde51fa6071fcdd41119e57aba65d4b4edee47665921d595df"
}
],
"buildCommands": [
@@ -230621,7 +230621,7 @@
"execution": {
"completed": true,
"sampleCount": 228,
"taskHalEventCount": 30
"taskHalEventCount": 35
}
},
"semanticBoundary": "web_task_hal_full_state"
@@ -231679,16 +231679,16 @@
{
"rel": "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js",
"absolutePath": "/home/mes123456/cnc_wams/wasm-port/build/wasm/task-hal/linuxcnc_task_hal.js",
"bytes": 19878,
"mtime": "2026-07-07T22:01:17.840Z",
"sha256": "b5f5caa1a898acea8280dfba6dae7b723d775e54e9d769bdb4d21ac6ac2ac358"
"bytes": 20858,
"mtime": "2026-07-08T14:15:38.726Z",
"sha256": "31a4b38ab6c92d5524c80414a159ae874f72bcbe4d3744019cefd18bb224c8f6"
},
{
"rel": "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm",
"absolutePath": "/home/mes123456/cnc_wams/wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm",
"bytes": 215042,
"mtime": "2026-07-07T22:01:17.596Z",
"sha256": "b456ffb64e5a32bc4a425343b5539dea9cf064b1804c4c6b53dea3661ea37fa8"
"bytes": 281624,
"mtime": "2026-07-08T14:15:38.526Z",
"sha256": "ed62ea4a6138c3bde51fa6071fcdd41119e57aba65d4b4edee47665921d595df"
}
],
"missing": [],