# G-code F 进给速度驱动 RUN 执行修复文档

生成时间：2026-06-22

## 1. 问题结论

当前 `RUN` 链路已经能完成：

```text
1. 打开 LinuxCNC G-code 程序。
2. 通过 task/HAL runtime 推进状态。
3. 更新当前行、高亮行、DRO、axisPose、执行轨迹和 task/HAL feedback。
```

但当前 `RUN` **还没有按 G-code 的真实进给速度执行**。

当前关键问题：

```text
G-code interpreter 能解析 F。
execution-timing.js 能基于 F / 距离 / INI 限速做 feed-based timing estimate。
但是 task/HAL RUN 执行链路没有把 F 用作真实运行速度。
C++ wrapper 层固定写入 velocity=60。
activeLine 按 task cycle 推进一行，而不是按 段距离 / F / elapsed time 推进。
```

因此现有浏览器 RUN 证据只能证明：

```text
task/HAL feedback 正在产生；
UI 当前行与 task/HAL active line 同步；
DRO 与 runtime axisPose 同步；
执行轨迹可见；
```

不能证明：

```text
程序按 G-code F 进给速度、G93/G94 模态、rapid/feed 区分、override、段距离真实定时执行。
```

## 2. 当前源码证据

### 2.1 固定 velocity=60

文件：

```text
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
```

函数：

```text
enqueue_linear_move_from_line(TaskRuntime &state, const std::string &line)
```

当前逻辑：

```cpp
command << ",\"velocity\":60}";
```

影响：

```text
1. 每条 G-code 运动行传给 motion runtime 的速度都是 60 units/s。
2. JS 状态层把 currentVel * 60 转成 mm/min。
3. 在 mm 单位下 UI 看到的速度固定为 3600 mm/min。
4. G-code 行中的 F159 / F318 / F636 等不会影响 RUN 实际速度。
```

### 2.2 按 task cycle 推进一行

文件：

```text
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
```

函数：

```text
lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles)
```

当前逻辑：

```cpp
for (int i = 0; i < task_cycles; ++i) {
    state.task_cycle += 1;
    if (state.interp_state == "READING" && state.next_program_line < state.opened_line_count) {
        enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
        ...
    }
    lcmot_step_servo(...);
}
```

影响：

```text
1. 每个 task cycle 至多 enqueue 一条程序行。
2. active line 由 cycle 数推进。
3. 长段、短段、F 快、F 慢不会改变行推进节奏。
4. 这不是真实 LinuxCNC planner/task/motion 的时间语义。
```

### 2.3 interpreter 已有 F 信息

文件：

```text
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-interpreter-runtime.js
```

已有能力：

```text
1. feedRatesBySourceLine(programText) 从 G-code 中提取 F。
2. parseLinuxCncCanonicalMotion(...) 把 activeFeedRate 写入 motion event 的 feedRate。
3. linearUnitsBySourceLine(...) 跟踪 G20/G21。
```

输出 motion event 已包含：

```js
{
  type,
  line,
  axes,
  feedRate,
  linearUnits,
  ...
}
```

### 2.4 timing estimate 已有 F 计算

文件：

```text
web-rtcp-5axis-sim-plan/app/src/runtime/execution-timing.js
```

已有能力：

```text
1. 按 motion event feedRate 计算 requestedLinearVelocity。
2. 按 profile/INI max velocity 限速。
3. 按 linear distance / angular distance 计算 segment durationSeconds。
4. 处理 feedOverride / rapidOverride。
```

但当前边界是：

```text
semanticBoundary = linuxcnc_canonical_motion_timing_estimate_not_planner_queue
```

说明它是估算，不是 task/HAL RUN 的权威执行节奏。

## 3. 修复目标

### 3.1 必须达成

修复后 `RUN` 必须满足：

```text
1. G-code 中 F 值进入 task/HAL RUN 执行链路。
2. G94 units/min 模式下，feed move 的速度来自当前 modal F。
3. G0/STRAIGHT_TRAVERSE 使用 rapid velocity / rapid override / INI max velocity。
4. activeLine 按 elapsed execution time 与当前段完成度推进。
5. 长距离低 F 段明显运行更久。
6. 短距离高 F 段明显更快完成。
7. UI currentVelocity 不再固定 3600 mm/min。
8. DRO/axisPose 可以在段内插值，而不是只在行边界跳变。
9. 现有 RUN gate、task/HAL session、status loop、STOP/ABORT/STEP 不回退。
```

### 3.2 第一阶段不强求

以下可以作为第二阶段：

```text
1. 完整 LinuxCNC trajectory planner queue 动力学一致性。
2. 加速度/jerk/圆弧真实插补完全对齐 LinuxCNC。
3. G93 inverse-time feed 的完整五轴角度/线性混合真实语义。
4. 硬件 realtime HAL 驱动。
```

但第一阶段至少不能继续固定 `velocity=60`。

## 4. 推荐实现方案

### 4.1 不要继续在 C++ wrapper 中逐行粗解析 G-code

当前 C++ wrapper 的 `enqueue_linear_move_from_line()` 是字符串扫描：

```text
查找 XYZABC 字母；
strtod 读取数值；
没有模态；
没有 F；
没有 G90/G91；
没有 G20/G21；
没有 G93/G94；
没有 G0/G1/G2/G3 区分；
```

这个方向继续扩展会很脆弱。

推荐改为：

```text
JS interpreter runtime 继续负责生成 canonical motion events。
JS 侧把 canonical motion + timing segments 传入 task/HAL runtime session。
C++ task/HAL wrapper 不再直接解析 G-code 行来生成 velocity。
C++ task/HAL wrapper 按已解析 motion segment 执行。
```

### 4.2 新增 task/HAL program motion plan

在 `wasm-port/runtime/sdk/src/linuxcnc-task-hal.js` 增加可选 API：

```js
loadProgramMotionPlan({
  programPath,
  motion,
  timing,
  linearUnits,
})
```

在 C++ wrapper 增加导出：

```cpp
int lctask_load_program_motion_plan_json(const char *plan_json);
```

motion plan 中至少包含：

```json
{
  "programPath": "...",
  "segments": [
    {
      "line": 8,
      "type": "STRAIGHT_FEED",
      "motionClass": "feed",
      "axes": { "x": 6.302, "y": -11.560, "z": 27.743, "a": -71.841, "c": -35.930 },
      "startAxes": { "...": 0 },
      "feedRate": 318,
      "linearUnits": "mm",
      "velocityMmPerMin": 318,
      "durationSeconds": 0.42,
      "startSeconds": 1.25,
      "elapsedSeconds": 1.67
    }
  ]
}
```

### 4.3 C++ task runtime 状态新增字段

`TaskRuntime` 增加：

```cpp
struct MotionSegment {
    int line = 0;
    std::string type;
    std::string motion_class;
    double start_seconds = 0.0;
    double duration_seconds = 0.0;
    double velocity_mm_per_min = 0.0;
    std::map<std::string, double> start_axes;
    std::map<std::string, double> end_axes;
};

std::vector<MotionSegment> motion_plan;
int active_segment_index = 0;
double run_elapsed_seconds = 0.0;
double run_start_seconds = 0.0;
```

### 4.4 RUN 周期推进改为按 elapsed time

当前：

```text
每 task cycle enqueue 一行。
```

修复后：

```text
每次 lctask_run_cycles 根据 task_period_ns * task_cycles 增加 run_elapsed_seconds。
根据 run_elapsed_seconds 找到 active segment。
按 segment progress 插值 axisPose。
把 currentVel/requestedVel 设置为 segment.velocityMmPerMin / 60。
activeLine = segment.line。
segment 完成后才进入下一 segment。
```

伪代码：

```cpp
int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles)
{
    const double delta_seconds = (task_period_ns / 1e9) * task_cycles;
    state.run_elapsed_seconds += delta_seconds;

    const MotionSegment *segment = find_segment_at_time(state.motion_plan, state.run_elapsed_seconds);
    if (!segment) {
        state.interp_state = "IDLE";
        state.exec_state = "DONE";
        return step_servo(...);
    }

    const double local = state.run_elapsed_seconds - segment->start_seconds;
    const double progress = clamp(local / segment->duration_seconds, 0.0, 1.0);
    AxisPose pose = interpolate(segment->start_axes, segment->end_axes, progress);

    forward_motion_sample({
        line: segment->line,
        axes: pose,
        currentVel: segment->velocity_mm_per_min / 60.0,
        requestedVel: segment->velocity_mm_per_min / 60.0,
        inPosition: progress >= 1.0
    });

    step_servo(...);
}
```

### 4.5 motion runtime command schema

当前 motion command 是：

```json
{
  "type": "EMC_TRAJ_LINEAR_MOVE",
  "line": 12,
  "x": 1,
  "velocity": 60
}
```

建议新增或扩展为：

```json
{
  "type": "EMC_TRAJ_LINEAR_SAMPLE",
  "line": 12,
  "x": 1.2,
  "y": 3.4,
  "z": 5.6,
  "a": -70,
  "c": 20,
  "currentVel": 5.3,
  "requestedVel": 5.3,
  "segmentProgress": 0.35,
  "source": "feed_timed_motion_plan"
}
```

如果不想新增 command type，也可以继续使用 `EMC_TRAJ_LINEAR_MOVE`，但必须：

```text
1. velocity 来自 segment.velocityMmPerMin / 60。
2. axes 是当前插值位置，而不是只用 segment end。
3. status 能保留 line/progress/currentVel。
```

## 5. G94 速度规则

### 5.1 G94 units per minute

在 `TRAJ.LINEAR_UNITS=mm` 时：

```text
F318 => 318 mm/min
```

在 `TRAJ.LINEAR_UNITS=inch` 或 G20 active 时：

```text
F10 => 10 inch/min => 254 mm/min
```

计算：

```text
velocity_mm_per_min = feedRate * linearUnitScaleToMm * feedOverride
duration_seconds = linearDistanceMm / (velocity_mm_per_min / 60)
```

### 5.2 Rapid

G0 / `STRAIGHT_TRAVERSE`：

```text
velocity_mm_per_min = min(INI max velocity, profile max velocity) * rapidOverride
duration_seconds = distance / velocity
```

### 5.3 Angular axes

第一阶段可沿用 `execution-timing.js` 现有策略：

```text
linearSeconds = linearDistanceMm / linearVelocity
angularSeconds = angularDistanceDeg / angularVelocity
durationSeconds = max(linearSeconds, angularSeconds)
```

这至少能避免旋转轴运动被零时长吞掉。

### 5.4 G93 inverse time

LinuxCNC impeller 程序包含：

```text
G93
...
G1 ... F159
```

G93 的 F 不是 units/min，而是 inverse time。第一阶段有两种策略：

```text
方案 A：先检测 G93，明确标注 unsupported，RUN gate 阻止真实 feed mode 运行。
方案 B：实现基础 inverse-time：duration_minutes = 1 / F，duration_seconds = 60 / F。
```

推荐：

```text
第一阶段实现方案 B。
```

原因：

```text
1. test_linuxcnc_source/impeller-7bl-xyzac.ngc 使用 G93。
2. 如果不支持 G93，就无法验证用户当前指定测试文件的真实进给语义。
3. 对 inverse-time，F 直接定义该运动块完成时间，适合第一阶段验证。
```

需要在 interpreter motion event 中增加：

```js
feedMode: "inverse-time" | "units-per-minute"
```

或至少在 timing 阶段通过 source line 扫描维护 G93/G94 模态。

## 6. 代码修改落点

### 6.1 `linuxcnc-interpreter-runtime.js`

新增：

```text
feedModeBySourceLine(programText)
```

识别：

```text
G93 => inverse-time
G94 => units-per-minute
```

motion event 增加：

```js
feedMode: activeFeedMode
```

### 6.2 `execution-timing.js`

修改 `buildTimingSegment(...)`：

```text
if event.feedMode === "inverse-time":
    durationSeconds = 60 / feedRate
    velocityMmPerMin = linearDistanceMm / durationSeconds * 60
else:
    保持 G94 units/min 逻辑
```

注意：

```text
1. G93 中 F 必须大于 0。
2. G93 中如果缺 F，应保留上一个 modal F 或报错，按 LinuxCNC 语义确认。
3. velocity 仍要可被 INI max velocity 限制还是忠实 inverse-time，需要明确。第一阶段建议记录 requested 与 capped 两个值。
```

### 6.3 `store.js`

在 `initializeTaskHalSession` 后或 `RUN_INTERPRETER_PROGRAM` 完成后，把当前 `programExecution.motion` 与 `programExecutionTiming.segments` 传给 task/HAL runtime：

```js
await state.taskHalRuntime.loadProgramMotionPlan({
  programPath: session.programPath,
  motion: state.programExecution.motion,
  timing: state.programExecutionTiming,
  linearUnits: state.profile.traj.linearUnits,
});
```

需要保证：

```text
1. LOAD_LINUXCNC_GCODE_SOURCE 后 interpreter 已完成。
2. task/HAL openProgram 的 programPath 与 motion plan programPath 一致。
3. 如果 motion plan 不存在，RUN gate 应阻止“真实进给速度 RUN”，不能悄悄回退 velocity=60。
```

### 6.4 `linuxcnc-task-hal.js`

新增 SDK 方法：

```js
loadProgramMotionPlan(plan) {
  const rc = callWithJson(mod, "lctask_load_program_motion_plan_json", plan);
  if (rc !== 0) throw new Error(...);
}
```

### 6.5 `linuxcnc_task_hal_wasm.hh`

新增声明：

```cpp
int lctask_load_program_motion_plan_json(const char *plan_json);
```

### 6.6 `linuxcnc_task_hal_wasm.cpp`

新增：

```text
1. MotionSegment 数据结构。
2. 简单 JSON plan parser，或复用现有轻量 json_number_after 风格解析数组。
3. lctask_load_program_motion_plan_json。
4. lctask_run_cycles 按 elapsed time 找 segment。
5. 删除或隔离 fixed velocity=60 的 fallback。
```

要求：

```text
fixed velocity=60 只能作为 explicit fixture fallback。
真实 RUN 路径不允许使用它。
```

## 7. 测试计划

### 7.1 Node 单元测试：F 值改变 duration

新增测试：

```text
web-rtcp-5axis-sim-plan/tests/node/verify_feed_rate_execution_timing.mjs
```

用例：

```gcode
G90 G94
G1 X10 F60
G1 X20 F600
M2
```

期望：

```text
第一段 10mm @ 60mm/min => 10s。
第二段 10mm @ 600mm/min => 1s。
duration ratio ≈ 10:1。
```

### 7.2 Node 单元测试：G93 inverse time

用例：

```gcode
G90 G93
G1 X10 F2
G1 X20 F10
M2
```

期望：

```text
F2 => 60/2 = 30s。
F10 => 60/10 = 6s。
```

### 7.3 task/HAL runtime 测试：速度不固定

新增或扩展：

```text
web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
```

断言：

```text
1. RUN feedback currentVelocityMmPerMin 不等于固定 3600。
2. 不同 F 段采样到不同 requested/current velocity。
3. activeLine 不再每 task cycle 固定递增一行。
4. 慢速段停留采样数 > 快速段。
```

### 7.4 浏览器证据测试

扩展：

```text
qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs
```

新增报告字段：

```text
feedMode
feedRate
segmentDurationSeconds
segmentProgress
requestedVelocityMmPerMin
currentVelocityMmPerMin
```

报告验收：

```text
1. impeller G93 行显示 inverse-time。
2. 采样 velocity 随当前 segment 变化。
3. 当前行停留时间与 F 值/段时长一致。
4. Word 报告中列出 F、feedMode、duration、velocity 的采样表。
```

## 8. 验收标准

修复完成必须通过：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_feed_rate_execution_timing.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linear_unit_conversion.mjs
npm --prefix web-rtcp-5axis-sim-plan/app run build
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
node qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs
```

并满足：

```text
1. 不再出现所有 RUN 样本 velocity=3600 的固定速度现象。
2. C++ task/HAL RUN 主路径不再写死 velocity=60。
3. G94 F60/F600 用例体现 10:1 段时长差异。
4. G93 F2/F10 用例体现 5:1 段时长差异。
5. test_linuxcnc_source/impeller-7bl-xyzac.ngc 的 RUN 报告包含 feedMode/feedRate/duration 证据。
6. STOP/ABORT/STEP 现有测试不回退。
```

## 9. 风险与注意事项

### 9.1 不要伪造 LinuxCNC 语义

如果还没有完整 planner，就必须在状态中明确：

```text
semanticBoundary = feed_timed_canonical_motion_runtime
```

不要标称为真实 LinuxCNC planner。

### 9.2 不要把 estimate 当作硬件实时

第一阶段可以做到：

```text
canonical motion + feed mode + timing driven browser simulation
```

不能声称：

```text
hardware realtime LinuxCNC execution
```

### 9.3 G93 是当前 impeller 文件的关键

`test_linuxcnc_source/impeller-7bl-xyzac.ngc` 开头有：

```gcode
M428 ;TCP:xyzac
G93
S600 M3
```

因此如果不处理 G93，针对该文件的“真实 F 速度”验证仍然不完整。

## 10. 建议实施顺序

```text
1. 给 interpreter motion event 增加 feedMode。
2. 给 execution-timing.js 增加 G93 duration。
3. 写 verify_feed_rate_execution_timing.mjs，先证明 timing 正确。
4. 给 task/HAL SDK/WASM 增加 loadProgramMotionPlan。
5. 改 lctask_run_cycles，按 elapsed time 和 segment progress 推进。
6. 改 store，把 motion plan 注入 task/HAL session。
7. 扩展 verify_run_feedback_loop，证明 velocity 不固定、慢段停留更久。
8. 扩展浏览器 RUN 报告，输出 feedMode/feedRate/duration/progress。
9. 重跑 build、smoke、浏览器报告。
10. 更新 working_run 和 gptlog-process。
```

