Add RUN precondition gate for 5-axis simulation

This commit is contained in:
2026-06-22 10:21:12 -04:00
parent ea8e10031b
commit 8ec662334f
10 changed files with 10537 additions and 16 deletions

View File

@@ -7,7 +7,7 @@
"build": "node scripts/build-static.mjs",
"dev": "python3 -m http.server 4173",
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
},
"dependencies": {},
"devDependencies": {}

View File

@@ -1064,11 +1064,12 @@ export function createSimulationStore(seed = {}) {
break;
}
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - 1, 0) },
], { taskCycles: 5, operatorMessage: "task/HAL program run" }).catch(() => {});
const preconditions = validateRunPreconditions(state, { requireTaskHalSession: false });
if (!preconditions.ok) {
setState({ operatorMessage: preconditions.operatorMessage });
break;
}
runValidatedTaskHalProgramRun().catch(() => {});
break;
}
const playback = nextProgramRuntimeSamplePlayback(state, 5);
@@ -1569,12 +1570,42 @@ export function createSimulationStore(seed = {}) {
return session;
};
const runValidatedTaskHalProgramRun = async () => {
const preflight = validateRunPreconditions(state, { requireTaskHalSession: false });
if (!preflight.ok) {
setState({ operatorMessage: preflight.operatorMessage });
return null;
}
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
await initializeTaskHalSession({ openProgram: true });
}
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
if (!ready.ok) {
setState({ operatorMessage: ready.operatorMessage });
return null;
}
return runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - Number(state.programStartLine || 1), 0) },
], {
taskCycles: 5,
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
};
const runTaskHalCommandSequence = async (commands, {
taskCycles = 1,
taskPeriodNs = 10000000,
servoPeriodNs = 1000000,
operatorMessage = "task/HAL command complete",
pendingJogCommand = null,
allowFixtureSession = true,
} = {}) => {
if (!state.taskHalRuntime?.loaded) {
throw new Error("LinuxCNC task/HAL runtime not attached");
@@ -1589,9 +1620,12 @@ export function createSimulationStore(seed = {}) {
try {
if (!state.taskHalSession && state.machineFileStaging?.save?.files?.length) {
await initializeTaskHalSession({ openProgram: true });
} else if (!state.taskHalSession && state.programLines?.length) {
} else if (allowFixtureSession && !state.taskHalSession && state.programLines?.length) {
await initializeFixtureTaskHalSessionForState();
}
if (!state.taskHalSession) {
throw new Error("LinuxCNC task/HAL session not initialized");
}
for (const command of commands) {
await state.taskHalRuntime.sendCommand(command);
}
@@ -1773,6 +1807,142 @@ function isAsyncKinematicsRuntime(runtime) {
return runtime?.executionContext === "worker";
}
export function validateRunPreconditions(state = {}, {
requireTaskHalRuntime = true,
requireTaskHalSession = true,
} = {}) {
const profile = state.profile || {};
const profileId = profile.id || state.machineProfile || "unknown";
const supportedProfiles = new Set(["xyzac-trt", "xyzbc-trt"]);
const fail = (operatorMessage, detail = {}) => ({
apiName: "web-rtcp-5axis-run-preconditions",
ok: false,
operatorMessage,
profileId,
iniPath: profile.iniPath || null,
coordinates: normalizeCoordinates(profile.traj?.coordinates),
kinematicsModuleId: profile.kinematicsModuleId || state.machineProfile || null,
...detail,
});
if (!supportedProfiles.has(profileId)) {
return fail(`run blocked: unsupported five-axis profile ${profileId}`);
}
if (!profile.iniPath || state.iniConfigReadiness?.loaded !== true || state.iniConfigReadiness?.ready !== true) {
return fail("run blocked: LinuxCNC INI not loaded");
}
if (state.linuxCncIniConfig?.path && state.linuxCncIniConfig.path !== profile.iniPath) {
return fail("run blocked: machine profile and INI path mismatch", {
iniPath: state.linuxCncIniConfig.path,
expectedIniPath: profile.iniPath,
});
}
const profileCoordinates = normalizeCoordinates(profile.traj?.coordinates);
const iniCoordinates = normalizeCoordinates(state.iniConfigReadiness?.coordinates || state.linuxCncIniConfig?.traj?.coordinates);
if (!profileCoordinates || !iniCoordinates || profileCoordinates !== iniCoordinates) {
return fail("run blocked: machine profile and INI coordinates mismatch", {
coordinates: iniCoordinates || null,
expectedCoordinates: profileCoordinates || null,
});
}
const profileKinematicsModuleId = profile.kinematicsModuleId || state.machineProfile || null;
const iniKinematicsModuleId = state.linuxCncIniConfig?.kinematicsModuleId || profileKinematicsModuleId;
if (!profileKinematicsModuleId || profileKinematicsModuleId !== iniKinematicsModuleId) {
return fail("run blocked: machine profile and INI kinematics mismatch", {
kinematicsModuleId: profileKinematicsModuleId,
expectedKinematicsModuleId: iniKinematicsModuleId,
});
}
const kinematicsReadiness = state.kinematicsRuntimeReadiness || {};
const runtimeKinematicsModuleId = kinematicsReadiness.moduleId || state.kinematicsRuntime?.moduleId || null;
if (kinematicsReadiness.loaded !== true || state.kinematicsRuntime?.loaded !== true) {
return fail("run blocked: LinuxCNC kinematics runtime not ready", {
kinematicsModuleId: profileKinematicsModuleId,
runtimeKinematicsModuleId,
});
}
if (runtimeKinematicsModuleId !== profileKinematicsModuleId) {
return fail("run blocked: LinuxCNC kinematics module mismatch", {
kinematicsModuleId: profileKinematicsModuleId,
runtimeKinematicsModuleId,
});
}
if (state.rtcpFrame?.sourceMode !== "source-derived-kinematics-wasm") {
return fail("run blocked: LinuxCNC kinematics frame not ready", {
frameSourceMode: state.rtcpFrame?.sourceMode || state.frameSourceMode || null,
});
}
if (state.machineFileStaging?.status !== "staged" || !state.machineFileStaging?.save?.files?.length) {
return fail("run blocked: LinuxCNC machine files not staged");
}
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
return fail("run blocked: no machine-file G-code opened for task/HAL session");
}
if (requireTaskHalRuntime) {
const taskHalReadiness = state.taskHalRuntimeReadiness || {};
const taskHalReady = state.taskHalRuntime?.loaded === true
&& taskHalReadiness.taskRuntimeReady === true
&& taskHalReadiness.motionRuntimeReady === true
&& taskHalReadiness.halRuntimeReady === true;
if (!taskHalReady) {
return fail("run blocked: task/HAL runtime not ready");
}
}
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (requireTaskHalSession) {
if (!state.taskHalSession?.programPath || !expectedProgramPath) {
return fail("run blocked: no machine-file G-code opened for task/HAL session");
}
if (state.taskHalSession.programPath !== expectedProgramPath) {
return fail("run blocked: task/HAL session program mismatch", {
programPath: state.taskHalSession.programPath,
expectedProgramPath,
});
}
}
return {
apiName: "web-rtcp-5axis-run-preconditions",
ok: true,
operatorMessage: null,
profileId,
machineType: profile.title || null,
iniPath: profile.iniPath,
coordinates: profileCoordinates,
kinematicsModuleId: profileKinematicsModuleId,
selectedGcodeSourceRel: state.machineFileStaging.selectedGcodeSourceRel,
programPath: expectedProgramPath,
sourceMode: "linuxcnc-task-motion-hal-wasm",
semanticBoundary: "linuxcnc_ini_profile_kinematics_task_hal_run_preconditions",
};
}
function expectedTaskHalProgramPathForState(state = {}) {
const sourceRel = state.machineFileStaging?.selectedGcodeSourceRel;
if (!sourceRel || !state.machineFileStaging?.plan || !state.machineFileStaging?.save) return null;
try {
return selectMachineFileProgram(
state.machineFileStaging.plan,
state.machineFileStaging.save,
sourceRel,
).wasmProgramPath || null;
} catch {
return null;
}
}
function normalizeCoordinates(value) {
return String(value || "").replace(/[^A-Za-z]/g, "").toUpperCase();
}
function applyTaskHalStatusPatch(state, status, operatorMessage) {
const ui = status?.ui || {};
const task = status?.task || {};

View File

@@ -4,6 +4,9 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { wrapTaskHalSdk } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
import { createSimulationStore } from "../../app/src/state/store.js";
@@ -20,21 +23,37 @@ const sdk = await createLinuxCncTaskHalSdk({
},
});
const runtime = wrapTaskHalSdk(sdk);
const store = createSimulationStore({
programLines: [
"G0 X1 Y2 Z-3 A10 C20",
"G1 X2 Y3 Z-4 A11 C21",
"M428",
"G1 X3 Y4 Z-5 A12 C22",
],
activeProgram: "task-hal-store-smoke.ngc",
const profile = getFiveAxisProfile("xyzac-trt");
const iniText = readFileSync(
resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath),
"utf8",
);
const iniConfig = parseLinuxCncIni(iniText, {
path: profile.iniPath,
profileId: profile.id,
});
const store = createSimulationStore();
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
store.dispatch({
type: "ATTACH_KINEMATICS_RUNTIME",
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
});
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime });
assert.equal(store.getState().taskHalRuntimeReadiness.taskRuntimeReady, true);
assert.equal(store.getState().taskHalRuntimeReadiness.motionRuntimeReady, true);
assert.equal(store.getState().taskHalRuntimeReadiness.halRuntimeReady, true);
await store.stageMachineFiles();
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
});
await waitForState(store, (state) => (
state.machineFileStaging.selectedGcodeSourceRel?.endsWith("xyzac_switchkins_test_1.ngc") &&
state.taskHalSession?.programPath?.endsWith("xyzac_switchkins_test_1.ngc")
));
store.dispatch({ type: "TOGGLE_POWER" });
await waitForTaskHal(store);
store.dispatch({ type: "HOME" });
@@ -47,11 +66,14 @@ let state = store.getState();
assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true);
assert.equal(state.taskHalStatus.summary.halSyncReady, true);
assert.equal(state.taskHalStatus.ui.activeLine >= 1, true);
assert.equal(state.axisPose.x >= 1, true);
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
assert.equal(state.fullExecutionBoundary.nativeTaskReady, true);
assert.equal(state.fullExecutionBoundary.nativeHalSyncReady, true);
await store.initializeTaskHalSession({ openProgram: true });
store.dispatch({ type: "TOGGLE_POWER" });
await waitForTaskHal(store);
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "mdi" });
await waitForTaskHal(store);
store.dispatch({ type: "RUN_MDI", command: "M428" });
@@ -111,6 +133,15 @@ async function waitForTaskHal(store) {
throw new Error("task/HAL store command did not settle");
}
async function waitForState(store, predicate) {
for (let attempt = 0; attempt < 60; attempt += 1) {
const state = store.getState();
if (predicate(state)) return state;
await new Promise((resolve) => setTimeout(resolve, 0));
}
throw new Error("store state condition did not settle");
}
function assertNear(actual, expected, message) {
assert.equal(Math.abs(Number(actual) - Number(expected)) < 1e-9, true, `${message}: ${actual} !== ${expected}`);
}

View File

@@ -0,0 +1,133 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
import {
applyIniConfigToProfile,
parseLinuxCncIni,
} from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { stageProfileMachineFiles } from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
import { buildTaskHalSessionFromMachineFiles } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
import {
createSimulationStore,
validateRunPreconditions,
} from "../../app/src/state/store.js";
const cases = [
{
profileId: "xyzac-trt",
machineName: "sim-xyzac-trt-kins (switchkins)",
coordinates: "XYZAC",
kinematicsName: "xyzac-trt-kins",
kinematicsModuleId: "xyzac-trt",
wasmFile: "linuxcnc_xyzac_trt_kinematics.wasm",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
},
{
profileId: "xyzbc-trt",
machineName: "sim-xyzbc-trt-kins (switchkins)",
coordinates: "XYZBC",
kinematicsName: "xyzbc-trt-kins",
kinematicsModuleId: "xyzbc-trt",
wasmFile: "linuxcnc_xyzbc_trt_kinematics.wasm",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
},
];
for (const item of cases) {
const baseProfile = getFiveAxisProfile(item.profileId);
const iniText = await readLinuxCncFile(baseProfile.iniPath);
const iniConfig = parseLinuxCncIni(iniText, {
path: baseProfile.iniPath,
profileId: item.profileId,
});
assert.equal(iniConfig.profileId, item.profileId);
assert.equal(iniConfig.machineName, item.machineName);
assert.equal(iniConfig.traj.coordinates, item.coordinates);
assert.equal(iniConfig.kinematics.name, item.kinematicsName);
assert.equal(iniConfig.kinematicsModuleId, item.kinematicsModuleId);
assert.equal(iniConfig.validation.ready, true);
const profile = applyIniConfigToProfile(baseProfile, iniConfig);
assert.equal(profile.traj.coordinates, item.coordinates);
assert.equal(profile.kinematicsModuleId, item.kinematicsModuleId);
assert.equal(profile.axisLimits[item.coordinates[0]].max > 0, true);
assert.equal(profile.jointConfig.length, item.coordinates.length);
const kinematicsRuntime = await createLinuxCncKinematicsRuntime({ moduleId: item.kinematicsModuleId });
assert.equal(kinematicsRuntime.readiness().moduleId, item.kinematicsModuleId);
assert.equal(kinematicsRuntime.wasmFile, item.wasmFile);
assert.equal(kinematicsRuntime.frameForJoints([10, 20, 30, 5, 15]).moduleId, item.kinematicsModuleId);
const staged = await stageProfileMachineFiles(profile);
const selectedPlan = staged.plan.selectedProgramSourceRel === item.sourceRel
? staged.plan
: {
...staged.plan,
wasmProgramPath: staged.save.files.find((file) => file.sourceRel === item.sourceRel)?.wasmPath,
selectedProgramSourceRel: item.sourceRel,
};
const session = buildTaskHalSessionFromMachineFiles({
profile,
plan: selectedPlan,
save: staged.save,
selectedProgramRel: item.sourceRel,
});
assert.equal(session.profileId, item.profileId);
assert.equal(session.iniPath.endsWith(`${item.profileId}.ini`), true);
assert.equal(session.programSourceRel, item.sourceRel);
assert.equal(session.programPath.endsWith(item.sourceRel.split("/").at(-1)), true);
}
const initialStore = createSimulationStore();
let check = validateRunPreconditions(initialStore.getState(), {
requireTaskHalRuntime: false,
requireTaskHalSession: false,
});
assert.equal(check.ok, false);
assert.equal(check.operatorMessage, "run blocked: LinuxCNC INI not loaded");
const profile = getFiveAxisProfile("xyzac-trt");
const iniText = await readLinuxCncFile(profile.iniPath);
const iniConfig = parseLinuxCncIni(iniText, {
path: profile.iniPath,
profileId: profile.id,
});
const store = createSimulationStore();
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
store.dispatch({
type: "ATTACH_KINEMATICS_RUNTIME",
runtime: await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }),
});
await store.stageMachineFiles();
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
});
check = validateRunPreconditions(store.getState(), {
requireTaskHalRuntime: false,
requireTaskHalSession: false,
});
assert.equal(check.ok, true);
assert.equal(check.profileId, "xyzac-trt");
assert.equal(check.coordinates, "XYZAC");
assert.equal(check.kinematicsModuleId, "xyzac-trt");
assert.equal(check.selectedGcodeSourceRel.endsWith("xyzac_switchkins_test_1.ngc"), true);
check = validateRunPreconditions(store.getState(), {
requireTaskHalRuntime: true,
requireTaskHalSession: false,
});
assert.equal(check.ok, false);
assert.equal(check.operatorMessage, "run blocked: task/HAL runtime not ready");
console.log("run_preconditions_ini_profile_smoke=ok");
console.log("run_preconditions_kinematics_smoke=ok");
console.log("run_preconditions_machine_file_smoke=ok");
async function readLinuxCncFile(sourceRel) {
return readFile(new URL(`../../../wasm-port/vendor/linuxcnc/${sourceRel}`, import.meta.url), "utf8");
}

View File

@@ -0,0 +1,570 @@
# 01 RUN 与 G-code 执行原理测试记录
生成时间2026-06-22
## 1. 参考接续文件
本记录按 `textbak` 中的接续要求核对,不把 UI fixture、自增行号、JS-owned CNC 语义当作真实运行证明。
关键约束来自:
```text
textbak/text2.txt
- 数控/G-code/remap/tool/parameter/planner/kinematics/user-M 语义必须来自 LinuxCNC upstream 或 vendored LinuxCNC C/C++ source。
- UI 已优先解析 run_stepfallback 到 canonical event。
- 不引入 JS G-code parser 或 JS remap/tool/parameter semantics。
textbak/text40.txt
- LinuxCNC 可加载 kinematics module 的独立 WASM ABI 已完成。
- 下一步应转向 Web/M4/M5 simulation UI 接入 createLinuxCncKinematicsSdk() 输出,或推进 remap/planner/browser 集成验证。
textbak/text41.txt
- Web/M4/M5 当前状态曾明确记录为 RUN/STEP/JOG/HOME 仍推进 fixture axis pose。
- 目标是 RUN/STEP/JOG/HOME 消费 LinuxCNC kinematics frame contract。
- program execution 如果仍是 fixture line playback必须继续标记为 fixture/interpreter-not-connected。
- remap/planner/browser full-process 不得因 kinematics 接入而自动 promotion。
```
当前源码已经比 `text41.txt` 的早期基线多接入了 interpreter、kinematics、task/HAL runtime`RUN` 的连续执行模型仍需要按 task/HAL feedback 口径复核。
## 2. 当前 G-code 加载过程
启动入口:
```text
app/src/main.js:17-23
```
页面启动时创建 store、挂载 gmoccapy UI并异步附加
```text
1. LinuxCNC INI config
2. LinuxCNC kinematics runtime
3. LinuxCNC interpreter runtime
4. LinuxCNC task/HAL runtime
5. machine file staging
```
默认程序加载:
```text
app/src/main.js:103-124
```
`ensureDefaultLinuxCncProgramPreview()` 在 interpreter 与 machine files staged 后选择默认 LinuxCNC 5-axis source然后 dispatch
```text
LOAD_LINUXCNC_GCODE_SOURCE
```
选择 vendored G-code 后store 的处理链是:
```text
app/src/state/store.js:744-792
```
实际步骤:
```text
1. 从 machineFileStaging.save.files 中找到 sourceRel 对应的 G-code 文本。
2. selectMachineFileProgram(...) 生成该 G-code 对应的 machine-file plan。
3. buildLoadedProgram(...) 将文本拆成 programLines设置 activeProgram、programStartLine=1、activeLine=1。
4. 设置 programSource=linuxcnc-vendored-5axis-gcode。
5. 如果 task/HAL runtime 已加载initializeTaskHalSession({ openProgram: true })。
6. 如果 interpreter runtime 已加载dispatch RUN_INTERPRETER_PROGRAM。
```
本地 operator 打开文件时走同一个 `buildLoadedProgram(...)`
```text
app/src/state/store.js:1033-1057
```
G-code 文本显示在 UI
```text
app/src/ui/gmoccapy-shell.js:193-228
```
这里使用 `state.programLines` 逐行渲染,`state.activeLine` 对应的行加 `.active`,同时显示:
```text
Current line ${state.activeLine}
${state.activeLine} / ${programEndLine}
```
所以“能否看到 G-code 程序”和“是否逐行显示当前行”取决于 `programLines``activeLine` 是否被同一执行反馈链正确更新。
## 3. interpreter 解释与 preview 过程
`RUN_INTERPRETER_PROGRAM` 不是机床运行按钮,它是在加载程序后调用 LinuxCNC interpreter runtime生成 canonical motion / summary / timing 基础数据:
```text
app/src/state/store.js:468-508
```
解释完成后:
```text
app/src/state/store.js:509-555
```
当前实现会:
```text
1. 保存 programExecution。
2. buildTimingForState(...) 构建 programExecutionTiming。
3. 设置 programExecutionSourceMode=execution.sourceMode。
4. 设置 programExecutionMotionIndex=0、programExecutionSampleIndex=0。
5. 根据 firstMotion 更新 activeLine、axisPose、kinsType、rtcpState。
6. 创建 firstFeedback并更新 feed.currentVelocity。
```
需要注意:这里的 `axisPoseFromCanonicalMotion(firstMotion, ...)` 来自 canonical motion 事件,是预览/初始反馈,不等价于运行态 task/HAL 连续反馈。
## 4. timing 与速度来源
`buildTimingForState(...)` 的优先级:
```text
app/src/state/store.js:2369-2379
```
如果 `execution.plannerTiming.plannerRuntimeReady === true`,使用 plannerTiming否则调用
```text
buildProgramExecutionTiming(...)
```
该 timing fallback 的语义边界明确是估算:
```text
app/src/runtime/execution-timing.js:50-53
semanticBoundary: linuxcnc_canonical_motion_timing_estimate_not_planner_queue
sourceBasis: LinuxCNC canonical motion events plus INI/profile velocity limits and feed overrides
```
fallback timing 的速度计算逻辑:
```text
app/src/runtime/execution-timing.js:75-123
```
它按 canonical motion 段计算:
```text
1. rapid 使用 maxLinearVelocity * rapidOverride。
2. feed 使用 feedRate * feedOverride。
3. linearSeconds = linearDistance / linearVelocity。
4. angularSeconds = angularDistance / angularVelocity。
5. durationSeconds = max(linearSeconds, angularSeconds)。
```
这不是 LinuxCNC task/motion planner queue 的真实 run_step 推进。除非 `plannerTiming` 已 ready否则“实际时间”和“实际进给速度”只能说是 canonical-based estimate。
## 5. RUN 按钮当前执行原理
`RUN` action 入口:
```text
app/src/state/store.js:1059-1100
```
分两条路径。
### 5.1 task/HAL runtime loaded 路径
如果 `state.taskHalRuntime?.loaded` 为 true`RUN` 执行:
```text
app/src/state/store.js:1066-1072
```
发送命令:
```text
EMC_TASK_SET_STATE ON
EMC_TASK_SET_MODE AUTO
EMC_TASK_PLAN_RUN line = activeLine - 1
```
然后调用:
```text
runTaskHalCommandSequence(..., { taskCycles: 5 })
```
`runTaskHalCommandSequence` 的实现:
```text
app/src/state/store.js:1572-1604
```
当前真实顺序是:
```text
1. 标记 taskHalExecutionPending=true。
2. 如未初始化 task/HAL session则 initializeTaskHalSession/openProgram。
3. 逐个 sendCommand(command)。
4. 调用 taskHalRuntime.runCycles({ taskCycles: 5 })。
5. 调用 taskHalRuntime.readStatus()。
6. dispatch TASK_HAL_STATUS_APPLIED。
```
结论:
```text
当前 RUN 是“发送 task/HAL 命令 -> 主动推进固定 cycles -> 拉取一次 status -> 应用到 UI”。
源码中没有看到 task/HAL worker 在程序运行期间主动持续 postMessage status 的订阅/推送通道。
```
### 5.2 task/HAL runtime missing fallback
如果 task/HAL runtime 未加载,`RUN` 走:
```text
app/src/state/store.js:1074-1098
```
即:
```text
nextProgramRuntimeSamplePlayback(state, 5)
```
这个 fallback 按 sampleIndex 或 motionIndex 一次跳 5 步,更新 activeLine / axisPose / feed / runtimeFeedback。
语义边界在 feedback 中标记为:
```text
linuxcnc-tp-runtime-sample
linuxcnc_tp_run_cycle_feedback_without_hardware
```
或在没有 sample 时退回:
```text
linuxcnc-canonical-motion
linuxcnc_canonical_motion_feedback_without_tp_sample
```
这条路径不能当作真实 task/HAL 运行过程。
## 6. task/HAL feedback 如何映射到 UI 状态
task/HAL runtime 包装:
```text
app/src/runtime/linuxcnc-task-hal-runtime.js:96-114
```
暴露的是:
```text
sendCommand(command)
runCycles(options)
readStatus()
```
worker client
```text
app/src/runtime/linuxcnc-task-hal-worker-client.js:14-28
```
worker 本体:
```text
app/src/runtime/linuxcnc-task-hal-worker.js:5-17
app/src/runtime/linuxcnc-task-hal-worker.js:19-53
```
worker 当前是 request/response 模型:
```text
主线程 postMessage({ id, type, payload })
worker 执行 handleMessage(type, payload)
worker postMessage({ id, ok, result })
```
没有看到类似:
```text
subscribeStatus
onStatus
while running post feedback
```
因此,当前 task/HAL feedback 不是“运行时持续推送”,而是 UI/store 调用 `readStatus()` 得到的一次快照。
状态归一化:
```text
app/src/runtime/linuxcnc-task-hal-runtime.js:155-199
```
`normalizeTaskHalStatus()` 从 motion/HAL pins 生成:
```text
ui.taskState
ui.taskMode
ui.interpState
ui.taskCycle
ui.servoCycle
ui.motionQueueDepth
ui.activeLine
ui.switchkinsType
ui.axisPose
ui.currentVelocity
```
应用到 store
```text
app/src/state/store.js:1776-1835
```
`applyTaskHalStatusPatch(...)` 将 status 映射为:
```text
activeLine = programStartLine + ui.activeLine - 1
axisPose = resolveTaskHalAxisPose(...)
kinsType = resolveTaskHalKinsType(...)
rtcpState = rtcpStateFromKinsType(kinsType)
runState = 根据 interpState / paused / aborted / complete 推导
feed.currentVelocity = ui.currentVelocity
programRuntimeFeedback = createTaskHalRuntimeFeedback(...)
```
runtime feedback 字段:
```text
app/src/state/store.js:1925-1948
```
包括:
```text
sourceMode=linuxcnc-task-motion-hal-wasm
sampleIndex=ui.servoCycle
motionIndex=ui.activeLine - 1
line=activeLine
timeSeconds=ui.taskCycle * 0.01
axisPose
currentVelocityMmPerMin
requestedVelocityMmPerMin
distanceToGo
queueDepth
cycle
taskCycle
```
这里的问题是:如果 `readStatus()` 不持续发生,这些字段不会按真实执行时间继续变化。
## 7. 本地快照观察
说明:本轮浏览器采集脚本在用户中断前已写出完整 JSON 快照,作为辅助证据。正式结论仍以源码追踪为主。
辅助证据文件:
```text
web-rtcp-5axis-sim-plan/working_run/run-feedback-evidence/run-feedback-local.json
web-rtcp-5axis-sim-plan/working_run/run-feedback-evidence/run-feedback-local.png
```
快照摘要:
| 快照 | runState | activeLine | servoCycle | velocity | axisPose |
| --- | --- | ---: | ---: | ---: | --- |
| loaded-default | idle | 1 | 0 | 0 | X0 Y0 Z0 A0 B0 C0 |
| after-power | idle | 1 | 10 | 0 | X0 Y0 Z0 A0 B0 C0 |
| after-home | idle | 1 | 10 | 0 | X43 Y-32.15 Z-11.306 A0 B0 C0 |
| after-auto | idle | 1 | 20 | 0 | X0 Y0 Z0 A0 B0 C0 |
| after-run-idle | running | 5 | 70 | 3600 | X0 Y0 Z5 A0 B0 C0 |
| run-plus-0.5s-no-command | running | 5 | 70 | 3600 | X0 Y0 Z5 A0 B0 C0 |
| run-plus-2.0s-no-command | running | 5 | 70 | 3600 | X0 Y0 Z5 A0 B0 C0 |
| run-plus-5.0s-no-command | running | 5 | 70 | 3600 | X0 Y0 Z5 A0 B0 C0 |
观察结论:
```text
1. 点击 RUN 后,状态从 line 1 推进到 line 5servoCycle 从 20 推进到 70。
2. 后续 0.5s / 2.0s / 5.0s 没有再次调用命令时activeLine、servoCycle、axisPose、velocity 均保持不变。
3. 这与源码中的 runCycles({ taskCycles: 5 }) + readStatus() 一次相符。
4. 这不是连续 task/HAL feedback 推送模型。
```
## 8. 发现的问题
### P1. RUN 没有持续消费 task/HAL feedback
期望:
```text
G-code 程序执行时task/HAL/motion runtime 持续产生状态;
UI/store 持续消费这些状态;
当前行、轴值、速度、DTG、程序时间来自同一个 feedback source。
```
当前:
```text
RUN 只主动推进固定 taskCycles=5然后 readStatus 一次。
没有看到 worker 主动推送 status。
没有看到 store 中存在运行期间持续 readStatus/readEvents 的执行循环。
```
影响:
```text
RUN 后页面可能停在某个 status snapshot。
用户看到 runState=running但 activeLine、轴值、速度不会随真实时间继续推进。
```
### P1. 轴值存在状态回退/覆盖风险
快照中可见:
```text
after-home: X43 Y-32.15 Z-11.306
after-auto: X0 Y0 Z0
```
源码位置:
```text
app/src/state/store.js:1874-1901
```
`resolveTaskHalAxisPose()``ui.axisPoseFrame === "work"` 直接合并 task/HAL axisPose因此 task/HAL status 中的 0 位姿可覆盖 HOME 后的 fixture/work pose。
这解释了“现实的轴值都不对”的一类现象UI 显示值可能来自 task/HAL local/status 快照,而不是实际期望的机床/工件坐标连续反馈。
### P2. 程序时间和速度可能是 estimate 或单点 status
fallback timing 明确是:
```text
linuxcnc_canonical_motion_timing_estimate_not_planner_queue
```
task/HAL feedback 中的时间:
```text
timeSeconds = ui.taskCycle * 0.01
```
如果 taskCycle 不推进,程序时间也不推进。快照中 `taskCycle=0`,但 velocity 已为 3600说明当前 runtime feedback 的时间/速度字段并不构成可靠连续执行轨迹。
### P2. “逐行执行过程”目前只显示当前 activeLine不显示执行历史
UI 已显示:
```text
programLines
activeLine
gcode-row.active
progress
```
但没有看到 execution log / visited line history / per-feedback transcript。
因此用户能看到程序文本和当前行,但看不到完整“一行一行执行过程”的历史记录。
## 9. 正确的 RUN 执行模型建议
按你提出的标准,正确模型不应靠前端定时器自造轨迹,而应由 task/HAL/motion runtime 产出状态。
建议目标模型:
```text
1. RUN 只发送 LinuxCNC task command
EMC_TASK_SET_STATE ON
EMC_TASK_SET_MODE AUTO
EMC_TASK_PLAN_RUN
2. task/HAL runtime 在程序执行期间持续 run task/motion/servo cycles。
3. 每个 cycle 或每批 cycle 产生 LinuxCNC-owned feedback
activeLine
statement/source line
axisPose
jointPose
currentVelocity
requestedVelocity
distanceToGo / DTG
queueDepth
interpState
cycle / taskCycle / servoCycle
switchkinsType / RTCP state
4. worker 将 feedback 推送给 main thread或 main thread 通过明确的 runtime pump 读取。
5. store 只消费 feedback不自行按 motionIndex/sampleIndex 推进真实运行态。
6. UI 渲染:
- G-code 当前行高亮来自 feedback.line
- DRO 轴值来自 feedback.axisPose 或 kinematics frame
- 速度来自 feedback.currentVelocity
- 程序时间来自 feedback.timeSeconds
- 执行历史记录追加每次 feedback。
```
注意这里的“pump”可以在 worker 内部完成,也可以由 main thread 调用 runtime pump但语义必须是 LinuxCNC task/HAL/motion cycles 产生反馈,而不是 UI 自己按 wall clock 或固定 index 插值。
## 10. 后续测试建议
建议在 `working_run` 后续补一个严格测试脚本,断言以下行为:
```text
1. 加载一个短 G-code
G90 G21
G0 X0 Y0 Z5
G1 X10 F600
G1 Y10 F300
M30
2. RUN 后采集至少 5 个 feedback tick。
3. 断言每个 tick 的 sourceMode 都是 linuxcnc-task-motion-hal-wasm。
4. 断言 activeLine 按 feedback 推进,而不是固定跳到 line 5 后停止。
5. 断言 servoCycle/taskCycle 单调递增。
6. 断言 axisPose 与当前 G-code motion 目标/插补位置一致。
7. 断言 currentVelocity 与当前运动段 F 值/override/task status 对齐。
8. 断言 UI `.gcode-row.active`、DRO、runtime feedback 文本和 store state 使用同一个 feedback snapshot。
9. 断言执行历史中能看到逐条 line/tick 记录。
```
## 11. 本轮验证命令
已运行:
```bash
npm --prefix web-rtcp-5axis-sim-plan/app run build
```
结果:
```text
gmoccapy_static_build=ok
```
浏览器采集脚本在用户中断前已产生完整快照 JSON未作为正式 smoke 通过声明;本报告只把它作为辅助观察。
## 12. 总结
当前 `web-rtcp-5axis-sim-plan` 的 G-code 程序加载、文本显示、interpreter canonical motion、task/HAL 命令发送和 status 映射链路已经存在。
但当前 `RUN` 的核心行为仍是:
```text
发送命令 -> runCycles 固定 5 个 task cycles -> readStatus 一次 -> 更新 UI 一次
```
它没有形成“G-code 执行期间 task/HAL feedback 持续推送/持续消费”的运行态闭环。因此,按实际时间和进给速度连续执行、真实轴值连续更新、逐行执行过程可见,这三项目前都不能判定为满足。

View File

@@ -0,0 +1,565 @@
# 02 RUN 前置条件、详细测试与编写步骤
生成时间2026-06-22
## 1. 核心原则
`RUN` 不是孤立按钮逻辑。对五轴机床,必须先确定 LinuxCNC 机床上下文,再执行程序。
正确顺序是:
```text
明确 INI 文件
-> 确定机床类型、坐标轴、joint、HAL、remap、tool-table
-> 确定五轴运动学算法和 switchkins 映射
-> 验证 kinematics WASM 和 task/HAL runtime 已处在同一机床上下文
-> 加载并打开 G-code
-> RUN 发送 LinuxCNC task command
-> 持续 runCycles/readStatus 或 worker push feedback
-> UI 只消费 task/HAL/motion feedback
```
不能先写一个通用 `RUN`,再猜测当前机床是什么。否则 `activeLine`、DRO、RTCP、A/B/C 旋转轴、TCP 补偿和 switchkins 状态都可能来自不同来源。
## 2. 必须先确认的机床上下文
当前项目内可作为 `RUN` 上下文的五轴 profile 只有两个:
| profile | LinuxCNC INI | 机床类型 | 坐标 | kinematics module |
| --- | --- | --- | --- | --- |
| `xyzac-trt` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | table rotary tilting | `XYZAC` | `xyzac-trt` |
| `xyzbc-trt` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | table rotary tilting | `XYZBC` | `xyzbc-trt` |
源码落点:
```text
app/src/profiles/index.js
app/src/profiles/xyzac-trt.js
app/src/profiles/xyzbc-trt.js
app/src/runtime/linuxcnc-ini-runtime.js
app/src/runtime/linuxcnc-kinematics-runtime.js
wasm-port/runtime/sdk/src/linuxcnc-kinematics.js
```
### 2.1 `xyzac-trt`
必须确认:
```text
profile id: xyzac-trt
INI: configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
[EMC] MACHINE: sim-xyzac-trt-kins (switchkins)
[TRAJ] COORDINATES: XYZAC
[KINS] KINEMATICS: xyzac-trt-kins sparm=identityfirst
WASM module id: xyzac-trt
WASM file: linuxcnc_xyzac_trt_kinematics.wasm
LinuxCNC source: src/emc/kinematics/xyzac-trt-kins.c
shared source: src/emc/kinematics/trtfuncs.c
switchkins source: src/emc/kinematics/switchkins.c
```
switchkins 映射:
```text
M429 -> switchkins type 0 -> identity
M428 -> switchkins type 1 -> tcp-xyzac
M430 -> switchkins type 2 -> userk
HAL link: motion.analog-out-03 => motion.switchkins-type
```
### 2.2 `xyzbc-trt`
必须确认:
```text
profile id: xyzbc-trt
INI: configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
[EMC] MACHINE: sim-xyzbc-trt-kins (switchkins)
[TRAJ] COORDINATES: XYZBC
[KINS] KINEMATICS: xyzbc-trt-kins sparm=identityfirst
WASM module id: xyzbc-trt
WASM file: linuxcnc_xyzbc_trt_kinematics.wasm
LinuxCNC source: src/emc/kinematics/xyzbc-trt-kins.c
shared source: src/emc/kinematics/trtfuncs.c
switchkins source: src/emc/kinematics/switchkins.c
```
switchkins 映射:
```text
M429 -> switchkins type 0 -> identity
M428 -> switchkins type 1 -> tcp-xyzbc
M430 -> switchkins type 2 -> userk
HAL link: motion.analog-out-03 => motion.switchkins-type
```
## 3. RUN 前置状态检查
`RUN` action 进入真正执行前,应要求这些状态全部明确:
```text
state.profile.id 是 xyzac-trt 或 xyzbc-trt
state.profile.iniPath 非空且已加载
state.iniConfigReadiness.loaded === true
state.iniConfigReadiness.ready === true
state.iniConfigReadiness.coordinates 与 profile.traj.coordinates 一致
state.profile.kinematicsModuleId 与 INI [KINS] KINEMATICS 推导结果一致
state.kinematicsRuntimeReadiness.loaded === true
state.kinematicsRuntimeReadiness.moduleId === state.profile.kinematicsModuleId
state.rtcpFrame.sourceMode === source-derived-kinematics-wasm
state.machineFileStaging.status === staged
state.taskHalRuntimeReadiness.taskRuntimeReady === true
state.taskHalRuntimeReadiness.motionRuntimeReady === true
state.taskHalRuntimeReadiness.halRuntimeReady === true
state.taskHalSession.programPath 指向当前选中的 G-code
```
如果任一项不满足,`RUN` 应返回明确 operatorMessage不应进入 fixture line playback 后伪装成真实运行。
建议阻断消息:
```text
run blocked: LinuxCNC INI not loaded
run blocked: machine profile and INI coordinates mismatch
run blocked: LinuxCNC kinematics runtime not ready
run blocked: task/HAL runtime not ready
run blocked: no machine-file G-code opened for task/HAL session
```
## 4. 详细测试计划
### 4.1 INI/profile 解析测试
目标:证明 `RUN` 使用的不是手写机床参数,而是明确 LinuxCNC INI。
测试文件建议:
```text
tests/node/verify_run_preconditions.mjs
```
断言:
```text
1. 读取 xyzac-trt.iniparseLinuxCncIni() 返回:
profileId=xyzac-trt
machineName=sim-xyzac-trt-kins (switchkins)
traj.coordinates=XYZAC
kinematics.name=xyzac-trt-kins
kinematicsModuleId=xyzac-trt
validation.ready=true
2. 读取 xyzbc-trt.iniparseLinuxCncIni() 返回:
profileId=xyzbc-trt
machineName=sim-xyzbc-trt-kins (switchkins)
traj.coordinates=XYZBC
kinematics.name=xyzbc-trt-kins
kinematicsModuleId=xyzbc-trt
validation.ready=true
3. applyIniConfigToProfile() 后:
profile.traj.coordinates 与 INI 一致
profile.kinematicsModuleId 与 INI 推导结果一致
axisLimits 和 jointConfig 来自 INI
```
命令:
```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
```
通过标准:
```text
run_preconditions_ini_profile_smoke=ok
```
### 4.2 运动学模块确认测试
目标:证明当前机床的五轴运动学算法已经按 INI/profile 加载。
断言:
```text
1. createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" }) 成功。
2. readiness.moduleId === "xyzac-trt"。
3. wasmFile === "linuxcnc_xyzac_trt_kinematics.wasm"。
4. frameForJoints([10,20,30,25,40]) 返回 moduleId=xyzac-trt。
5. switchKinematics(1) 后 switchkinsType=1frame source 仍是 linuxcnc_kinematics_wasm_c_abi。
6. 对 xyzbc-trt 重复同样测试。
```
已有相关测试:
```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
```
新增 RUN 前置测试应把这个结果纳入 `RUN` gate而不是只作为独立 smoke。
### 4.3 machine-file staging 测试
目标:证明 `RUN` 打开的程序、INI、remap、tool table 是同一个 LinuxCNC 机床目录上下文。
断言:
```text
1. stageProfileMachineFiles(profile) 成功。
2. save.files 中包含:
- 当前 profile 的 INI
- 当前 profile 的 tool table
- remap_subs/428remap.ngc
- remap_subs/429remap.ngc
- remap_subs/430remap.ngc
- 当前 profile 的 demo G-code
3. buildTaskHalSessionFromMachineFiles() 生成:
profileId 与 state.profile.id 一致
iniPath 与 profile.iniPath 一致
programPath 指向当前 selectedGcodeSourceRel
```
已有相关测试:
```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs
```
### 4.4 RUN 连续 feedback 测试
目标:证明点击 `RUN` 后,不是只推进一次 `taskCycles=5`,而是持续从 task/HAL/motion 获取反馈。
测试程序:
```ngc
G90 G21
M429
G0 X0 Y0 Z5 A0 C0
M428
G1 X10 Y0 Z5 A10 C20 F600
G1 X10 Y10 Z0 A20 C30 F300
M429
G1 X0 Y0 Z5 A0 C0 F600
M30
```
`xyzbc-trt` 时把 A 轴替换为 B 轴:
```ngc
G90 G21
M429
G0 X0 Y0 Z5 B0 C0
M428
G1 X10 Y0 Z5 B10 C20 F600
G1 X10 Y10 Z0 B20 C30 F300
M429
G1 X0 Y0 Z5 B0 C0 F600
M30
```
采集至少 8 个 feedback tick断言
```text
1. 每个 tick 的 sourceMode 都是 linuxcnc-task-motion-hal-wasm。
2. taskCycle 或 servoCycle 单调递增。
3. activeLine 随 feedback 推进,不停留在 RUN 后第一帧。
4. axisPose 来自同一 tick 的 task/HAL status.ui.axisPose。
5. currentVelocityMmPerMin 来自同一 tick 的 motion currentVel。
6. M428 后 kinsType 切到 tcp-xyzac 或 tcp-xyzbc。
7. M429 后 kinsType 回到 identity。
8. UI 当前高亮行、DRO、runtime feedback 文本读取的是同一个 feedback snapshot。
9. 程序完成后 runState 变为 complete 或 idle不继续显示 running。
```
失败判定:
```text
RUN 后 servoCycle 只增加一次,然后 0.5s/2s/5s 不变。
activeLine 固定在某一行不再推进。
axisPose 从 HOME 位姿被 task-local 0 值覆盖。
programRuntimeFeedback.sourceMode 不是 linuxcnc-task-motion-hal-wasm。
RTCP 状态与当前 G-code 的 M428/M429 不一致。
```
### 4.5 浏览器端测试
建议扩展:
```text
qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
```
新增 case
```text
07-run-preconditions-and-feedback
```
浏览器断言:
```text
1. 首屏等待 state.iniConfigReadiness.loaded === true。
2. 选择 xyzac-trt确认 titlebar/profile/INI/kinematics module 都是 xyzac-trt。
3. 点击 POWER、HOME、AUTO。
4. 加载上述短 G-code。
5. 点击 RUN。
6. 采集 t=0.2s、0.5s、1.0s、2.0s、5.0s 状态。
7. 断言 task/servo cycle 单调递增。
8. 断言 UI `.gcode-row.active` 的 data-program-line 等于 state.activeLine。
9. 断言 DRO X/Y/Z/A/C 等于 state.programRuntimeFeedback.axisPose。
10. 断言 canvas dataset 的 RTCP 状态与 state.rtcpState 一致。
```
输出证据:
```text
qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json
qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/*.png
```
## 5. 编写 RUN 程序的实现步骤
### Step 1: 增加 `validateRunPreconditions(state)`
位置建议:
```text
app/src/state/store.js
```
职责:
```text
1. 检查 INI 已加载。
2. 检查 profile 与 INI 坐标一致。
3. 检查 profile.kinematicsModuleId 与 kinematics runtime moduleId 一致。
4. 检查 kinematics runtime ready。
5. 检查 task/HAL runtime ready。
6. 检查 machine file staging ready。
7. 检查 taskHalSession.programPath 指向当前 G-code。
```
返回结构:
```js
{
ok: true,
profileId: "xyzac-trt",
iniPath: ".../xyzac-trt.ini",
coordinates: "XYZAC",
kinematicsModuleId: "xyzac-trt"
}
```
失败时:
```js
{
ok: false,
operatorMessage: "run blocked: LinuxCNC INI not loaded"
}
```
### Step 2: `RUN` action 先调用前置检查
当前 `RUN` 不能直接进入:
```text
runTaskHalCommandSequence([...], { taskCycles: 5 })
```
应先:
```text
1. gateLinuxCncTaskAction(state, action)
2. validateRunPreconditions(state)
3. initializeTaskHalSession({ openProgram: true }) 如需要
4. 确认 readStatus() 的 openProgram/programPath 与 state.activeProgram 对齐
```
未满足前置条件时,只设置 `operatorMessage`,不推进任何 line/sample。
### Step 3: 拆分 task command 与执行 pump
`RUN` 应只发送 LinuxCNC task 命令:
```text
EMC_TASK_SET_STATE ON
EMC_TASK_SET_MODE AUTO
EMC_TASK_PLAN_RUN line = activeLine - programStartLine
```
然后启动 feedback pump
```text
while running:
taskHalRuntime.runCycles({ taskCycles: batchSize })
status = taskHalRuntime.readStatus()
dispatch TASK_HAL_STATUS_APPLIED(status)
append feedback history
if complete/paused/aborted/stopped:
stop pump
```
说明:
```text
这里的 pump 可以在 store 主线程调度,也可以放到 worker 内部 postMessage 推送。
关键不是 timer 本身,而是每个 tick 都必须由 task/HAL/motion runtime 的 runCycles/readStatus 产生反馈。
```
### Step 4: 增加 RUN pump 状态
建议 state 增加:
```js
taskHalRunPump: {
active: false,
sequence: 0,
profileId: null,
iniPath: null,
kinematicsModuleId: null,
tickCount: 0,
lastStatusAt: null,
lastError: null
}
```
同时增加:
```js
programRuntimeFeedbackHistory: []
```
历史最多保留 100 到 500 条,避免浏览器长程序内存无限增长。
### Step 5: `TASK_HAL_STATUS_APPLIED` 只消费 status
`applyTaskHalStatusPatch()` 应继续负责:
```text
activeLine
axisPose
kinsType
rtcpState
runState
feed.currentVelocity
programRuntimeFeedback
```
但需要补充:
```text
1. programRuntimeFeedbackHistory append 当前 feedback。
2. 记录 feedback.profileId、iniPath、kinematicsModuleId。
3. 对 ui.axisPoseFrame 做严格处理,避免 task-local 0 值覆盖 HOME/work pose。
4. 运行态 axisPose、velocity、DTG、activeLine 必须来自同一个 status snapshot。
```
### Step 6: STOP/ABORT/PAUSE/RESUME 控制 pump
行为要求:
```text
STOP/ABORT:
send EMC_TASK_ABORT
stop pump
read final status
PAUSE:
send EMC_TASK_PLAN_PAUSE
pump 可停止或降频读取 paused status
runState=paused
RESUME:
send EMC_TASK_PLAN_RESUME
restart pump
STEP:
不走 fixture sample playback
发送/推进一个明确的 task cycle batch
readStatus 一次
runState=stepping 或 paused
```
### Step 7: UI 显示执行历史
`app/src/ui/gmoccapy-shell.js` 的 info/sidebar 区域增加:
```text
Last feedback:
sourceMode / sample / line / taskCycle / servoCycle / velocity / kinsType
Run history:
最近 10 条 line/tick
```
UI 只显示 `programRuntimeFeedbackHistory`,不自行推导行号或坐标。
### Step 8: 移除真实 RUN 的 fixture fallback 混淆
保留 fallback 可以用于开发,但必须明确标记:
```text
programExecutionSourceMode=fixture-line-playback
operatorMessage=LinuxCNC task/HAL unavailable; fixture playback only
fullLinuxCncProgramExecutionReady=false
```
在真实 `taskHalRuntime.loaded === true` 且前置条件失败时,不应退回 fixture playback。
## 6. 开发验收命令
基础检查:
```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
npm --prefix web-rtcp-5axis-sim-plan/app run build
```
完整 node smoke
```bash
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
```
浏览器 smoke
```bash
bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh
```
QA 证据采集:
```bash
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
```
## 7. 完成标准
`RUN` 可以判定为完善,必须同时满足:
```text
1. 每次 RUN 都能追溯到明确 INI。
2. 每次 RUN 都能说明机床类型和坐标轴。
3. 每次 RUN 都能说明使用哪个 LinuxCNC 五轴 kinematics module。
4. M428/M429/M430 的 switchkins 状态来自 LinuxCNC remap/HAL/task feedback 链。
5. activeLine、DRO、速度、DTG、程序时间来自同一个 task/HAL/motion feedback snapshot。
6. RUN 后 taskCycle/servoCycle 随时间持续推进,直到 paused/stopped/complete。
7. UI 高亮行和执行历史能复现逐行执行过程。
8. fallback playback 不能冒充 LinuxCNC task/HAL runtime。
```
未满足以上条件时,状态应继续标记为:
```text
fullLinuxCncProgramExecutionReady=false
hardwareDrive=false
hostRealtimeKernel=false
externalUserMProcessReady=false
```

View File

@@ -0,0 +1,311 @@
# 03 RUN 第一步完成记录与下一步建议
生成时间2026-06-22
## 1. 本轮完成范围
本轮只完成 `02-run-preconditions-test-and-implementation-steps.md` 中的第一步和第二步:
```text
Step 1: 增加 validateRunPreconditions(state)
Step 2: RUN action 先调用前置检查
```
本轮没有实现连续 feedback pump。也没有把 `RUN` 宣称为完整实时运行模型。
## 2. 已完成改动
### 2.1 RUN 前置条件 gate
新增源码:
```text
app/src/state/store.js
export function validateRunPreconditions(...)
```
校验内容:
```text
1. profile 必须是 xyzac-trt 或 xyzbc-trt。
2. LinuxCNC INI 必须已加载并 validation.ready=true。
3. profile.iniPath 必须与已加载 INI path 一致。
4. profile TRAJ coordinates 必须与 INI coordinates 一致。
5. profile.kinematicsModuleId 必须与 INI [KINS] 推导结果一致。
6. LinuxCNC kinematics runtime 必须 loaded。
7. kinematics runtime moduleId 必须匹配当前 profile。
8. RTCP frame 必须来自 source-derived-kinematics-wasm。
9. machine files 必须 staged。
10. 必须已选择 LinuxCNC source-directory G-code。
11. task/HAL runtime 必须 ready。
12. requireTaskHalSession=true 时taskHalSession.programPath 必须匹配当前 selected G-code。
```
关键结果:
```text
当 taskHalRuntime 已加载时RUN 不再直接发送 EMC_TASK_PLAN_RUN。
RUN 会先校验 INI/profile/kinematics/machine-file/task-HAL 上下文。
前置条件不满足时,只给 operatorMessage不进入 fixture playback。
```
### 2.2 RUN 使用真实 machine-file session
新增内部流程:
```text
runValidatedTaskHalProgramRun()
```
执行顺序:
```text
1. validateRunPreconditions(..., requireTaskHalSession=false)
2. 如 taskHalSession 缺失或 programPath 不匹配initializeTaskHalSession({ openProgram: true })
3. validateRunPreconditions(..., requireTaskHalSession=true)
4. 发送:
EMC_TASK_SET_STATE ON
EMC_TASK_SET_MODE AUTO
EMC_TASK_PLAN_RUN line = activeLine - programStartLine
5. runTaskHalCommandSequence(..., allowFixtureSession=false)
```
这一步解决的是“RUN 之前先确定明确 INI、机床类型和五轴运动学算法”不是连续执行问题。
### 2.3 新增测试
新增:
```text
tests/node/verify_run_preconditions.mjs
```
覆盖:
```text
1. xyzac-trt.ini 解析:
MACHINE=sim-xyzac-trt-kins (switchkins)
COORDINATES=XYZAC
KINEMATICS=xyzac-trt-kins
kinematicsModuleId=xyzac-trt
2. xyzbc-trt.ini 解析:
MACHINE=sim-xyzbc-trt-kins (switchkins)
COORDINATES=XYZBC
KINEMATICS=xyzbc-trt-kins
kinematicsModuleId=xyzbc-trt
3. applyIniConfigToProfile() 后 profile 与 INI 保持一致。
4. createLinuxCncKinematicsRuntime() 可按 moduleId 加载对应 WASM。
5. stageProfileMachineFiles() 和 buildTaskHalSessionFromMachineFiles() 能形成同一 machine-file 上下文。
6. validateRunPreconditions() 能阻断未加载 INI 的初始状态。
7. validateRunPreconditions() 能在 INI/kinematics/staged G-code ready 时通过非 task-HAL session 预检。
8. task/HAL runtime 缺失时返回明确阻断消息。
```
`app/package.json``smoke:node` 已加入:
```text
node ../tests/node/verify_run_preconditions.mjs
```
### 2.4 调整 task/HAL smoke
更新:
```text
tests/node/verify_linuxcnc_task_hal_runtime.mjs
```
调整点:
```text
1. 不再用内联 fixture G-code 直接测试真实 RUN。
2. 加载 xyzac-trt INI。
3. 加载 xyzac-trt kinematics runtime。
4. stage machine files。
5. LOAD_LINUXCNC_GCODE_SOURCE 选择:
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc
6. 等待 taskHalSession.programPath 指向该 G-code。
7. 再执行 POWER/HOME/AUTO/RUN。
```
这样 task/HAL RUN smoke 与新的 RUN 前置条件一致。
## 3. 已运行验证
通过:
```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
npm --prefix web-rtcp-5axis-sim-plan/app run build
```
输出:
```text
run_preconditions_ini_profile_smoke=ok
run_preconditions_kinematics_smoke=ok
run_preconditions_machine_file_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
gmoccapy_static_build=ok
```
部分通过后失败:
```bash
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
```
该命令已通过本轮新增和相关测试:
```text
linuxcnc_kinematics_runtime_smoke=ok
linuxcnc_interpreter_runtime_smoke=ok
linuxcnc_ini_runtime_smoke=ok
run_preconditions_ini_profile_smoke=ok
run_preconditions_kinematics_smoke=ok
run_preconditions_machine_file_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
```
随后在既有 native audit 停止:
```text
wasm-port/tests/native/verify_task_hal_phase0.sh failed with status 1
```
直接运行该脚本确认失败点是:
```text
wasm-port/tools/verify_task_hal_source_manifest.sh
```
日志:
```text
task_hal_source_manifest_ready=0
task_hal_reference_source_ready=0
task_hal_missing_reference_source_count=20
```
说明:该失败来自外部 reference source `../linuxcnc` 缺失 task/HAL manifest 需要的 20 个文件,不是本轮 RUN 前置条件改动造成。
## 4. 当前 RUN 状态
现在 `RUN` 的第一道边界已经收紧:
```text
没有明确 INI -> 不 RUN
profile/INI 坐标不一致 -> 不 RUN
kinematics module 不一致 -> 不 RUN
kinematics frame 未 ready -> 不 RUN
machine files 未 staged -> 不 RUN
没有 LinuxCNC source G-code -> 不 RUN
task/HAL runtime 未 ready -> 不 RUN
taskHalSession.programPath 不匹配当前 G-code -> 不 RUN
```
但当前 `RUN` 仍然是:
```text
发送 task command
-> runCycles({ taskCycles: 5 })
-> readStatus() 一次
-> TASK_HAL_STATUS_APPLIED 一次
```
所以 `01-run-gcode-execution-principle-and-test.md` 中指出的连续 feedback 问题仍未解决。
## 5. 下一步工作建议
下一步应实现 `02` 文件中的 Step 3 和 Step 4
```text
Step 3: 拆分 task command 与执行 pump
Step 4: 增加 RUN pump 状态
```
建议最小实现范围:
```text
1. 在 store state 增加 taskHalRunPump:
active
sequence
profileId
iniPath
kinematicsModuleId
tickCount
lastStatusAt
lastError
2. 增加 programRuntimeFeedbackHistory最多保留 100 条。
3. RUN 发送 EMC_TASK_PLAN_RUN 后,不只 runCycles 5 次。
改为启动 pump:
runCycles({ taskCycles: batchSize })
readStatus()
dispatch TASK_HAL_STATUS_APPLIED
append feedback history
根据 interpState / task.nextProgramLine / openedLineCount / paused / aborted 停止。
4. STOP/ABORT 必须停止 pump。
5. PAUSE 停止或降频 pump并保留 paused status。
6. RESUME 重新启动 pump。
7. STEP 在 task/HAL runtime loaded 时不走 fixture sample playback
而是推进一个明确 task cycle batch 并 readStatus 一次。
```
下一步测试建议:
```text
1. 新增 tests/node/verify_run_feedback_pump.mjs。
2. 使用 xyzac_switchkins_test_1.ngc。
3. RUN 后采集至少 8 个 feedback tick。
4. 断言 taskCycle 或 servoCycle 单调递增。
5. 断言 activeLine 不停留在 RUN 后第一帧。
6. 断言每个 tick 的 sourceMode 都是 linuxcnc-task-motion-hal-wasm。
7. 断言 programRuntimeFeedbackHistory 至少包含 3 条。
8. 断言 STOP 后 pump.active=false。
```
浏览器测试建议:
```text
扩展 qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
新增 07-run-preconditions-and-feedback case。
采集 RUN 后 0.2s / 0.5s / 1.0s / 2.0s 的 state。
断言 activeLine、DRO、runtime feedback、canvas RTCP 状态来自同一 feedback snapshot。
```
## 6. 重要边界
下一步实现 pump 时仍需保持:
```text
不引入 JS G-code parser。
不在 UI 里自增 activeLine。
不在 UI 里自造 axisPose。
不把 canonical timing estimate 当作真实 task/HAL runtime。
不把 fixture playback 标记为 fullLinuxCncProgramExecutionReady。
```
RUN 的真实推进必须来自:
```text
taskHalRuntime.runCycles()
taskHalRuntime.readStatus()
TASK_HAL_STATUS_APPLIED
```

View File

@@ -0,0 +1,54 @@
# working_run 运行功能测试与原理记录
生成时间2026-06-22
本目录记录 `web-rtcp-5axis-sim-plan` 本地项目中 G-code 程序加载、解释、`RUN` 执行链路、task/HAL feedback 消费方式,以及当前测试发现的问题。
## 文件清单
| 文件 | 用途 |
| --- | --- |
| `01-run-gcode-execution-principle-and-test.md` | G-code 具体执行过程、`RUN` 执行原理、源码证据、测试观察和问题结论 |
| `02-run-preconditions-test-and-implementation-steps.md` | `RUN` 前置条件、INI/机床/五轴运动学确认流程、详细测试用例和编写 `RUN` 程序步骤 |
| `03-run-step1-preconditions-complete-next-steps.md` | 本轮已完成的 `RUN` 前置条件 gate、验证结果和下一步接续建议 |
## 结论摘要
`RUN` 不应作为第一步实现。正确顺序必须是:
```text
1. 先确定明确的 LinuxCNC INI 文件。
2. 由 INI/profile 确定机床类型、坐标轴、joint 配置、HAL/remap/tool-table 资产。
3. 由 INI 的 [KINS] KINEMATICS 确定五轴运动学算法和 switchkins 映射。
4. 验证对应 LinuxCNC kinematics WASM 已加载,并能产生 source-derived frame。
5. 之后才进入 RUN打开同一 INI 上下文下的 G-code发送 task command持续消费 task/HAL/motion feedback。
```
当前项目明确支持的五轴机床上下文是:
| profile | INI | 机床类型 | 坐标 | 运动学 |
| --- | --- | --- | --- | --- |
| `xyzac-trt` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | table rotary tilting | `XYZAC` | `xyzac-trt-kins sparm=identityfirst` |
| `xyzbc-trt` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | table rotary tilting | `XYZBC` | `xyzbc-trt-kins sparm=identityfirst` |
当前实现已经能显示 G-code 程序文本、当前行、高亮行、DRO、runtime feedback 和 task/HAL 状态。
但按 `textbak` 接续文件的边界要求,当前 `RUN` 还不是连续的 task/HAL feedback 推送执行模型:
```text
RUN 点击后:
1. UI 发送 EMC_TASK_SET_STATE / EMC_TASK_SET_MODE / EMC_TASK_PLAN_RUN
2. store 调用 taskHalRuntime.runCycles({ taskCycles: 5 })
3. 随后 readStatus() 一次;
4. TASK_HAL_STATUS_APPLIED 将这一次 status 映射到 activeLine / axisPose / velocity / feedback
5. 没有看到 worker 主动持续 postMessage 推送 status
6. 如果没有再次调用 runCycles/readStatus状态不会继续随真实时间推进。
```
因此,当前问题重点不是“前端是否需要定时器”,而是:
```text
task/HAL runtime 是否能在 G-code 执行期间持续产出状态;
UI/store 是否持续消费这些 LinuxCNC-owned 状态;
轴值、当前行、速度、DTG、程序时间是否都来自同一条 task/HAL/motion feedback 链。
```

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB