Files
cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS暂停按钮LinuxCNC调用链分析.md
2026-07-07 09:40:33 -04:00

695 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AXIS 主界面暂停按钮 LinuxCNC 调用链分析
## 目的
本文分析 `/home/mes123456/cnc_wams/linuxcnc` 源码中 AXIS 主界面“暂停按钮”的完整调用链,包括 UI、Python 扩展、NML task 命令、task 状态机、motion 命令和轨迹规划器 TP 的实现。目标是为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的暂停按钮修正提供对标依据。
结论先行AXIS 的暂停不是只改一个 `runState`,而是同时维护两层暂停语义:
1. task/interpreter 暂停:`EMC_TASK_PLAN_PAUSE` 使 `task.interpState = PAUSED`,保存 `interpResumeState`,并设置 `task.task_paused = 1`
2. motion/trajectory 暂停task 调用 `emcTrajPause()`motion 收到 `EMCMOT_PAUSE` 后调用 `tpPause()`,轨迹规划器把 `tp->pausing = 1`,后续插补以速度目标 0 做受控减速,并通过 `motion.traj.paused` 对外反馈。
因此 Web 暂停按钮要同时冻结“解释器继续读行”和“运动采样继续推进”,并保留恢复前的解释器状态。
## 调用链总览
AXIS 工具栏暂停按钮链条:
```text
share/axis/tcl/axis.tcl
.toolbar.program_pause -command task_pauseresume
-> src/emc/usr_intf/axis/scripts/axis.py
commands.task_pauseresume()
-> linuxcnc.command().auto(AUTO_PAUSE 或 AUTO_RESUME)
-> src/emc/usr_intf/axis/extensions/emcmodule.cc
emcauto()
-> 发送 EMC_TASK_PLAN_PAUSE / EMC_TASK_PLAN_RESUME NML 命令
-> src/emc/task/emctaskmain.cc
emcTaskPlan() / emcTaskIssueCommand()
-> pause: emcTrajPause(); interpState=PAUSED; task_paused=1
-> resume: emcTrajResume(); interpState=interpResumeState; task_paused=0
-> src/emc/task/taskintf.cc
emcTrajPause() / emcTrajResume()
-> 写 EMCMOT_PAUSE / EMCMOT_RESUME 到 motion
-> src/emc/motion/command.c
EMCMOT_PAUSE: tpPause(); emcmotStatus->paused=1
EMCMOT_RESUME: tpResume(); emcmotStatus->paused=0
-> src/emc/tp/tp.c
tpPause(): tp->pausing=1
tpResume(): tp->pausing=0
```
菜单 Pause 和 Resume 是分开的命令;工具栏按钮是同一个 `task_pauseresume`,会根据当前状态决定暂停还是恢复。
## AXIS UI 层
### Tcl 工具栏按钮
`linuxcnc/share/axis/tcl/axis.tcl:543-549` 定义工具栏暂停按钮:
```tcl
Button .toolbar.program_pause \
-command task_pauseresume \
-helptext [_ "Pause \[P\] / resume \[S\] execution"] \
-image [load_image tool_pause]
```
这个按钮不是固定 Pause也不是固定 Resume而是 toggle 行为:运行时点击发送 pause已暂停时点击发送 resume。
同一文件 `axis.tcl:131-139` 定义菜单项:
```tcl
.menu.machine add command -accelerator P -command task_pause
.menu.machine add command -accelerator S -command task_resume
```
也就是说:
- 菜单 Pause 调 `task_pause`
- 菜单 Resume 调 `task_resume`
- 工具栏 Pause 图标调 `task_pauseresume`
Web 项目当前工具栏 `tbtn_pause` 对标的是 `task_pauseresume`,菜单 Pause/Resume 才应分别对标 `task_pause``task_resume`
### AXIS Python 命令入口
`linuxcnc/src/emc/usr_intf/axis/scripts/axis.py:2402-2407`
```python
def task_pause(*event):
if s.task_mode != linuxcnc.MODE_AUTO or s.interp_state not in (linuxcnc.INTERP_READING, linuxcnc.INTERP_WAITING):
return
ensure_mode(linuxcnc.MODE_AUTO)
c.auto(linuxcnc.AUTO_PAUSE)
```
菜单 Pause 的门槛很严格:
- 必须是 `MODE_AUTO`
- `interp_state` 必须是 `INTERP_READING``INTERP_WAITING`
- 满足后发送 `AUTO_PAUSE`
`axis.py:2424-2431`
```python
def task_resume(*event):
s.poll()
if not s.paused:
return
if s.task_mode not in (linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI):
return
ensure_mode(linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI)
c.auto(linuxcnc.AUTO_RESUME)
```
菜单 Resume 的关键判断不是 `interp_state == PAUSED`,而是 `s.paused`。在 AXIS Python stat 中,`s.paused` 对应 motion/traj 层的 paused 状态,而 `s.task_paused` 是 task 层状态。两者都要关注。
`axis.py:2433-2443`
```python
def task_pauseresume(*event):
if s.task_mode not in (linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI):
return
ensure_mode(linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI)
s.poll()
if s.paused:
if resume_inhibit: return
c.auto(linuxcnc.AUTO_RESUME)
elif s.interp_state != linuxcnc.INTERP_IDLE:
c.auto(linuxcnc.AUTO_PAUSE)
```
工具栏 toggle 的真实语义:
- 如果 motion/traj 已 paused发送 `AUTO_RESUME`
- 否则只要 interpreter 不是 idle发送 `AUTO_PAUSE`
- 如果 interpreter idle点击无效
- 允许 `MODE_AUTO``MODE_MDI`
- 恢复还会受 `resume_inhibit` 影响
`axis.py:906-910` 周期性把 LinuxCNC stat 写入 Tcl 变量:
- `vars.task_paused <- self.stat.task_paused`
- `vars.interp_pause <- self.stat.paused`
这说明 AXIS UI 同时观察 task 暂停和 motion 暂停。
键盘绑定在 `axis.py:3163-3165`
- `s` -> `commands.task_resume`
- `p` -> `commands.task_pause`
## Python 扩展和 NML 命令
AXIS 的 `linuxcnc.command().auto()` 在 C++ Python 扩展中实现。
`linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc:68-73` 定义本地 AUTO 常量:
```cpp
#define LOCAL_AUTO_RUN (0)
#define LOCAL_AUTO_PAUSE (1)
#define LOCAL_AUTO_RESUME (2)
#define LOCAL_AUTO_STEP (3)
```
`emcmodule.cc:2093-2129``emcauto()` 根据参数创建并发送 NML 命令:
- `LOCAL_AUTO_PAUSE` -> `EMC_TASK_PLAN_PAUSE`
- `LOCAL_AUTO_RESUME` -> `EMC_TASK_PLAN_RESUME`
- `LOCAL_AUTO_STEP` -> `EMC_TASK_PLAN_STEP`
关键片段:
```cpp
case LOCAL_AUTO_PAUSE:
emcSendCommand(s, pause);
break;
case LOCAL_AUTO_RESUME:
emcSendCommand(s, resume);
break;
case LOCAL_AUTO_STEP:
emcSendCommand(s, step);
break;
```
NML 类型定义在 `linuxcnc/src/emc/nml_intf/emc.hh:127-135`
- `EMC_TASK_PLAN_PAUSE_TYPE = 510`
- `EMC_TASK_PLAN_STEP_TYPE = 511`
- `EMC_TASK_PLAN_RESUME_TYPE = 512`
NML 消息类定义在 `linuxcnc/src/emc/nml_intf/emc_nml.hh:1276-1327`。这三个消息类没有额外字段,核心信息就是消息类型本身。
## LinuxCNC 状态字段
解释器状态定义在 `linuxcnc/src/emc/nml_intf/emc.hh:220-226`
```cpp
enum class EMC_TASK_INTERP {
IDLE = 1,
READING = 2,
PAUSED = 3,
WAITING = 4
};
```
motion/traj 状态字段在 `linuxcnc/src/emc/nml_intf/emc_nml.hh:965-991`
- `EMC_TRAJ_STAT::paused`
- `EMC_TRAJ_STAT::single_stepping`
- `EMC_TRAJ_STAT::queue`
- `EMC_TRAJ_STAT::activeQueue`
- `EMC_TRAJ_STAT::id`
task 状态字段在 `linuxcnc/src/emc/nml_intf/emc_nml.hh:1448-1471`
- `EMC_TASK_STAT::interpState`
- `EMC_TASK_STAT::task_paused`
AXIS Python 扩展把这些字段暴露给 Python
- `emcmodule.cc:1137` 暴露 `interp_state`
- `emcmodule.cc:1148` 暴露 `task_paused`
- `emcmodule.cc:3380-3383` 暴露 `INTERP_IDLE/READING/PAUSED/WAITING`
## task 层实现
`linuxcnc/src/emc/task/emctaskmain.cc:15-35` 说明 task 主循环原则:
1. 周期性调用 `emcTaskPlan()``emcTaskExecute()`
2. `emcTaskPlan()` 读取新命令,并按机器模式和状态决定处理方式。
3. AUTO 模式下解释器会把命令追加到 `interp_list`
4. `emcTaskExecute()` 根据 precondition/postcondition 从 `interp_list` 取命令执行。
5. immediate command 不走 interp list pre/postcondition。
这解释了一个关键现象:暂停可能作为 GUI 即时命令被处理,也可能作为解释器列表中的 pause/optional stop 被排队处理。
### 保存恢复状态
`emctaskmain.cc:427`
```cpp
static EMC_TASK_INTERP interpResumeState = EMC_TASK_INTERP::IDLE;
```
暂停时如果当前不是 PAUSED就保存当前解释器状态恢复时还原
- 暂停保存:`emctaskmain.cc:2337-2343`
- 恢复还原:`emctaskmain.cc:2369-2375`
### PLAN_PAUSE 执行点
`emctaskmain.cc:2337-2345`
```cpp
case EMC_TASK_PLAN_PAUSE_TYPE:
emcTrajPause();
if (emcStatus->task.interpState != EMC_TASK_INTERP::PAUSED) {
interpResumeState = emcStatus->task.interpState;
}
emcStatus->task.interpState = EMC_TASK_INTERP::PAUSED;
emcStatus->task.task_paused = 1;
retval = 0;
break;
```
这就是 task 层暂停的核心:
- 先暂停轨迹:`emcTrajPause()`
- 保存恢复前解释器状态:`interpResumeState`
- 设置解释器状态为 `PAUSED`
- 设置 `task_paused = 1`
### PLAN_RESUME 执行点
`emctaskmain.cc:2369-2377`
```cpp
case EMC_TASK_PLAN_RESUME_TYPE:
emcTrajResume();
emcStatus->task.interpState = interpResumeState;
emcStatus->task.task_paused = 0;
emcStatus->motion.traj.single_stepping = 0;
stepping = 0;
steppingWait = 0;
retval = 0;
break;
```
恢复时:
- 先恢复轨迹:`emcTrajResume()`
- 把解释器状态恢复到暂停前的 `interpResumeState`
-`task_paused`
- 清单步状态
### PAUSED 状态下不继续取解释器队列
`emctaskmain.cc:2614-2624``EMC_TASK_EXEC::DONE` 分支中只有当 `interpState != PAUSED` 时才继续从 `interp_list` 取下一条命令:
```cpp
if (!emcStatus->motion.traj.queueFull &&
emcStatus->task.interpState != EMC_TASK_INTERP::PAUSED) {
emcTaskCommand = interp_list.get();
}
```
这对 Web 很重要:暂停时不仅速度为 0还必须停止继续推进解释器命令和 UI sample。否则会出现“按钮显示暂停但 G-code 行号、sampleIndex 或刀具位置继续走”的问题。
### precondition 中的排队暂停
`emctaskmain.cc:1569-1572`
```cpp
case EMC_TASK_PLAN_PAUSE_TYPE:
case EMC_TASK_PLAN_OPTIONAL_STOP_TYPE:
return EMC_TASK_EXEC::WAITING_FOR_MOTION_AND_IO;
```
如果 pause 命令来自解释器列表,它会等前面的 motion 和 IO 完成后再执行。这不同于用户点击 GUI 时的 immediate pause。Web 如果模拟 M0/M1 或解释器排队暂停,应等当前 motion/IO 条件满足;如果模拟 AXIS 工具栏点击,则应立即向 task/motion 发暂停请求。
### STEP 与暂停的关系
`emctaskmain.cc:1254-1265`:在 PAUSED 状态下收到 STEP如果 motion queue 里有暂停的 motion就调用 `emcTrajStep()`,否则恢复 interpreter 到 `interpResumeState`
`emctaskmain.cc:2538-2549``STEPPING_CHECK()``stepping/steppingWait/steppedLine` 控制只执行一步。
这说明 STEP 不是普通 resume也不是永远推进一个 UI sample它是“在 paused 状态下短暂运行,直到 motion id/line 改变后再暂停”。
## task 到 motion 的接口
`linuxcnc/src/emc/task/taskintf.cc:1417-1422`
```cpp
int emcTrajPause()
{
emcmotCommand.command = EMCMOT_PAUSE;
return usrmotWriteEmcmotCommand(&emcmotCommand);
}
```
`taskintf.cc:1438-1449`
```cpp
int emcTrajStep()
{
emcmotCommand.command = EMCMOT_STEP;
return usrmotWriteEmcmotCommand(&emcmotCommand);
}
int emcTrajResume()
{
emcmotCommand.command = EMCMOT_RESUME;
return usrmotWriteEmcmotCommand(&emcmotCommand);
}
```
task 层不自己停止电机轨迹,它把 `EMCMOT_PAUSE/RESUME/STEP` 写给 motion。
motion 状态回读在 `taskintf.cc:1678`
```cpp
stat->paused = emcmotStatus.paused;
```
这就是 AXIS 的 `s.paused` 来源。
## motion 层实现
motion 命令枚举在 `linuxcnc/src/emc/motion/motion.h:94-104`
- `EMCMOT_PAUSE`
- `EMCMOT_REVERSE`
- `EMCMOT_FORWARD`
- `EMCMOT_RESUME`
- `EMCMOT_STEP`
motion 状态字段 `emcmotStatus->paused``motion.h:631-640` 定义,初始化在 `motion.c:940-951` 清零。
`linuxcnc/src/emc/motion/command.c:1234-1240`
```c
case EMCMOT_PAUSE:
tpPause(&emcmotInternal->coord_tp);
emcmotStatus->paused = 1;
break;
```
`command.c:1256-1263`
```c
case EMCMOT_RESUME:
emcmotStatus->stepping = 0;
tpResume(&emcmotInternal->coord_tp);
emcmotStatus->paused = 0;
break;
```
`command.c:1265-1274`
```c
case EMCMOT_STEP:
if(emcmotStatus->paused) {
emcmotInternal->idForStep = emcmotStatus->id;
emcmotStatus->stepping = 1;
tpResume(&emcmotInternal->coord_tp);
emcmotStatus->paused = 1;
}
```
STEP 的 motion 语义是:在 paused 时记录当前 motion id短暂 resume TP但状态仍视为 paused等 id 改变后自动再 pause。
`linuxcnc/src/emc/motion/control.c:2211-2218`
```c
if (emcmotStatus->stepping && emcmotInternal->idForStep != emcmotStatus->id) {
tpPause(&emcmotInternal->coord_tp);
emcmotStatus->stepping = 0;
emcmotStatus->paused = 1;
}
```
这就是 STEP 自动回到暂停的实现。
## 轨迹规划器 TP 实现
TP 结构字段在 `linuxcnc/src/emc/tp/tp_types.h:117-126`
```c
int nextId;
int execId;
int done;
int depth;
int activeDepth;
int aborting;
int pausing;
int reverse_run;
```
TP API 在 `linuxcnc/src/emc/tp/tp.h:45-48`
```c
int tpRunCycle(TP_STRUCT * tp, long period);
int tpPause(TP_STRUCT * tp);
int tpResume(TP_STRUCT * tp);
int tpAbort(TP_STRUCT * tp);
```
`linuxcnc/src/emc/tp/tp.c:4225-4240`
```c
int tpPause(TP_STRUCT * const tp)
{
if (0 == tp) return TP_ERR_FAIL;
tp->pausing = 1;
return TP_ERR_OK;
}
int tpResume(TP_STRUCT * const tp)
{
if (0 == tp) return TP_ERR_FAIL;
tp->pausing = 0;
return TP_ERR_OK;
}
```
暂停本质是设置 `tp->pausing`。真正的减速逻辑在规划循环里体现。
`tp.c:247-258`
```c
bool pausing = tp->pausing && (tc->synchronized == TC_SYNC_NONE || tc->synchronized == TC_SYNC_VELOCITY);
if (pausing) {
return 0.0;
}
```
暂停时 feed scale 返回 0使目标速度降到 0。
`tp.c:2832-2838`
```c
bool is_pausing = tp->pausing && (...);
bool use_velocity_control = (is_pausing || is_aborting || emcmotStatus->net_feed_scale <= TP_VEL_EPSILON);
```
暂停/abort/feed override 为 0 都进入 velocity-control 风格的减速规划。
`tp.c:4132-4135` 注释说明如果 aborting 或 pausing 且速度到 0就不需要继续规划并返回 stopped。
所以 LinuxCNC 的 motion pause 不是把位置瞬间锁死,也不是清空队列,而是让当前规划器以受控方式减速到 0到达 paused 稳态后,`current_vel` 为 0队列和当前 id 保留,恢复时从原队列继续。
## Web 当前实现的对照问题
从当前 Web 项目看,相关文件主要是:
- `app/src/ui/axis-shell.js`
- `app/src/state/linuxcnc-task-policy.js`
- `app/src/state/store.js`
Web 工具栏暂停按钮在 `axis-shell.js:170-182` 渲染,`tbtn_pause` 绑定 action `pause-resume``axis-shell.js:736-748` 将其分发为 `PAUSE_RESUME`,这与 AXIS 工具栏对标方向正确。
Web 当前 `isProgramPaused()``axis-shell.js:616-620` 使用:
```js
state.runState === "paused" ||
state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true
```
这少了一个 LinuxCNC 真实关键字段motion/traj paused也就是 AXIS 的 `s.paused`。Web 如果只看 `taskPaused``interpState`,可能无法准确模拟“运动层已经 paused但 task 状态尚未完全同步”的短窗口。
Web policy 在 `linuxcnc-task-policy.js:161-188` 对 PAUSE/RESUME 的 gating 大体接近 AXIS
- 菜单 PAUSE 要求 task on、auto、interp reading/waiting。
- pause-resume 允许 auto/mdiinterp idle 时忽略。
- RESUME 要求 interp paused。
但 AXIS `task_resume()``task_pauseresume()` 的恢复判断用的是 `s.paused`,即 motion/traj paused。Web 当前恢复更多依赖 `interpState === "paused"`。建议增加或统一 `machine.motionPaused` / `programRuntimeFeedback.paused` 字段,并将 resume gate 改为:
```text
canResume = taskState on
&& taskMode in auto/mdi
&& (motionPaused || interpState paused || taskPaused)
&& !resumeInhibit
```
Web `store.js:1760-1807` 的 PAUSE 分支已经设置:
- `interpState: "paused"`
- `taskPaused: true`
- `runState: "paused"`
- `feed.currentVelocity: 0`
- `programRuntimeFeedback` velocity 归零
但要严格对标 LinuxCNC还需要确保暂停后不再推进
- `programExecutionSampleIndex`
- `programUiExecution.sampleIndex`
- `programRuntimeFeedback.sampleIndex`
- `activeLine/currentLine`
- `axisPose`
- `DRO`
- `toolhead/toolAxis`
- task/HAL status loop 的采样推进
LinuxCNC 的关键保护是 `emctaskmain.cc:2614-2624``interpState == PAUSED` 时不从 `interp_list` 取下一条。Web 也需要同等约束:所有周期性 playback/status loop 在 paused 时必须只 poll 状态,不推进 sample 或解释器队列。
## Web 修正建议
### 1. 拆清三种状态
建议 Web 状态显式拆成:
```js
machine: {
interpState: "idle" | "reading" | "paused" | "waiting",
interpResumeState: "idle" | "reading" | "waiting",
taskPaused: boolean,
motionPaused: boolean,
resumeInhibit: boolean
}
```
其中:
- `taskPaused` 对标 `EMC_TASK_STAT::task_paused`
- `motionPaused` 对标 `EMC_TRAJ_STAT::paused` / AXIS `s.paused`
- `interpState` 对标 `EMC_TASK_STAT::interpState`
- `interpResumeState` 对标 task 层静态变量 `interpResumeState`
### 2. 工具栏 toggle 必须按 AXIS 判断
对标 `axis.py:2433-2443`
```js
if (taskMode not in auto/mdi) return;
if (motionPaused || taskPaused || interpState === "paused") {
if (resumeInhibit) return;
dispatch RESUME;
} else if (interpState !== "idle") {
dispatch PAUSE;
}
```
其中恢复优先看 `motionPaused`,不是只看 `interpState`
### 3. PAUSE 动作必须先冻结 motion再冻结 interpreter
对标 `emctaskmain.cc:2337-2343`
```js
motionPaused = true;
if (interpState !== "paused") interpResumeState = interpState;
interpState = "paused";
taskPaused = true;
runState = "paused";
```
同时不应修改当前 `sampleIndex`、当前行、DRO 或 toolhead 位置,只把速度反馈归零。
### 4. RESUME 动作必须恢复到 interpResumeState
对标 `emctaskmain.cc:2369-2375`
```js
motionPaused = false;
interpState = interpResumeState || "reading";
taskPaused = false;
singleStepping = false;
runState = interpState === "reading" ? "running" : "idle";
```
如果 `interpResumeState``waiting`,恢复后应回到 waiting再由执行循环完成后转 idle不要一律改成 reading。
### 5. STEP 不是普通 sample+1
对标 `command.c:1265-1274``control.c:2211-2218`
Web STEP 应记录当前 motion id/sample id短暂允许推进到下一个 motion id 或下一条解释器线,然后重新置:
- `interpState = "paused"`
- `taskPaused = true`
- `motionPaused = true`
- `runState = "paused"` 或短暂 `"stepping"` 后回 `"paused"`
当前 Web 的 `STEP` 分支会推进 sample这个方向可以保留但结束状态必须回到 paused并且不应变成长期 running。
### 6. 暂停时 status loop 只能刷新状态,不可推进执行
Web 的 task/HAL status loop 和 fallback playback 都要加硬条件:
```js
if (state.machine.interpState === "paused" ||
state.machine.taskPaused ||
state.machine.motionPaused ||
state.runState === "paused") {
return freezeCurrentExecutionPatch(state);
}
```
冻结 patch 应保留当前 sample/pose/DRO/toolhead只允许
- velocity -> 0
- paused flags -> true
- operator message/status timestamp 更新
## 验收断言建议
暂停按钮要通过以下断言才算对标 LinuxCNC
1. 工具栏点击运行中程序后:
- `runState === "paused"`
- `machine.interpState === "paused"`
- `machine.taskPaused === true`
- `machine.motionPaused === true`
- `feed.currentVelocity === 0`
2. 暂停后保持 5 秒:
- `sampleIndex` 不变
- `activeLine/currentLine` 不变
- `axisPose` 不变
- DRO 不变
- canvas toolhead/tool axis 不变
- runtime TCP 不变
3. 再点工具栏暂停按钮:
-`resumeInhibit` false则恢复到 `interpResumeState`
- `motionPaused === false`
- `taskPaused === false`
4. 菜单 Pause 在 `interpState === "idle"` 时必须无效。
5. 菜单 Resume 在 `motionPaused/taskPaused/interpState paused` 之外必须无效。
6. STEP 后必须重新进入 paused 稳态,不允许持续 running。
## 最小调用链索引
| 层级 | 文件 | 关键行 | 作用 |
| --- | --- | --- | --- |
| Tcl UI | `linuxcnc/share/axis/tcl/axis.tcl` | 543-549 | 工具栏 pause/resume 按钮绑定 `task_pauseresume` |
| 菜单 UI | `linuxcnc/share/axis/tcl/axis.tcl` | 131-139 | 菜单 Pause/Resume 分别绑定 `task_pause`/`task_resume` |
| AXIS Python | `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py` | 2402-2443 | 判断状态并调用 `c.auto(AUTO_PAUSE/RESUME)` |
| Python stat | `axis.py` | 906-910 | 更新 `task_paused``interp_pause` UI 变量 |
| Python 扩展 | `linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc` | 2093-2129 | `AUTO_PAUSE/RESUME/STEP` 转 NML 命令 |
| NML 类型 | `linuxcnc/src/emc/nml_intf/emc.hh` | 127-135 | 定义 `EMC_TASK_PLAN_PAUSE/RESUME/STEP_TYPE` |
| NML 类 | `linuxcnc/src/emc/nml_intf/emc_nml.hh` | 1276-1327 | 定义 task pause/resume/step 消息类 |
| task 状态 | `linuxcnc/src/emc/nml_intf/emc.hh` | 220-226 | 定义 `IDLE/READING/PAUSED/WAITING` |
| task 主循环 | `linuxcnc/src/emc/task/emctaskmain.cc` | 15-35 | 说明 plan/execute、interp_list、immediate command 架构 |
| task pause | `linuxcnc/src/emc/task/emctaskmain.cc` | 2337-2345 | 调 `emcTrajPause`,设置 `interpState/task_paused` |
| task resume | `linuxcnc/src/emc/task/emctaskmain.cc` | 2369-2377 | 调 `emcTrajResume`,恢复 `interpResumeState` |
| task execute freeze | `linuxcnc/src/emc/task/emctaskmain.cc` | 2614-2624 | PAUSED 时不继续取 `interp_list` |
| task->motion | `linuxcnc/src/emc/task/taskintf.cc` | 1417-1449 | 发送 `EMCMOT_PAUSE/STEP/RESUME` |
| motion command | `linuxcnc/src/emc/motion/command.c` | 1234-1274 | 调 `tpPause/tpResume`,维护 `emcmotStatus->paused` |
| motion step | `linuxcnc/src/emc/motion/control.c` | 2211-2218 | STEP 运动 id 改变后自动再 pause |
| TP API | `linuxcnc/src/emc/tp/tp.c` | 4225-4240 | `tpPause/tpResume` 设置 `tp->pausing` |
| TP 减速 | `linuxcnc/src/emc/tp/tp.c` | 247-258, 2832-2838 | pausing 时 feed scale/velocity control 使速度目标为 0 |
## 最终判断
LinuxCNC AXIS 暂停按钮可靠的根本原因是UI 只发命令task 层负责解释器状态和队列冻结motion 层负责轨迹暂停TP 层负责受控减速到 0。Web 项目的暂停如果“一直不好使用”,通常不是按钮绑定本身的问题,而是没有完整复现这四层状态同步:
- 没有 `motionPaused` 等价字段或没有用它做恢复判断;
- 暂停后 status/playback loop 仍推进 sample
- `interpResumeState` 没有严格保留和恢复;
- STEP 被实现成普通推进,而不是短暂恢复后再暂停;
- 菜单 Pause、菜单 Resume、工具栏 Pause/Resume toggle 的门槛混在一起。
修正 Web 时应优先把暂停状态机改成 LinuxCNC 的双层模型:`task_paused/interpState` 管解释器,`motionPaused/currentVelocity` 管运动,执行循环在 paused 时冻结取样与队列推进。