# 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/mdi,interp 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 时冻结取样与队列推进。