Validate AXIS task state flow
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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
Binary file not shown.
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 全部工作收口复验
|
||||
|
||||
### 本轮目标
|
||||
|
||||
@@ -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 验收。
|
||||
|
||||
@@ -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` 中全部工作,并重新执行收口复验。
|
||||
|
||||
@@ -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` 的状态机和真实程序执行结果。
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user