# 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_step，fallback 到 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 5，servoCycle 从 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 持续推送/持续消费”的运行态闭环。因此，按实际时间和进给速度连续执行、真实轴值连续更新、逐行执行过程可见，这三项目前都不能判定为满足。
