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

23 KiB
Raw Blame History

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 工具栏暂停按钮链条:

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 定义工具栏暂停按钮:

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 定义菜单项:

.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_pausetask_resume

AXIS Python 命令入口

linuxcnc/src/emc/usr_intf/axis/scripts/axis.py:2402-2407

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_READINGINTERP_WAITING
  • 满足后发送 AUTO_PAUSE

axis.py:2424-2431

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

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_AUTOMODE_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 常量:

#define LOCAL_AUTO_RUN (0)
#define LOCAL_AUTO_PAUSE (1)
#define LOCAL_AUTO_RESUME (2)
#define LOCAL_AUTO_STEP (3)

emcmodule.cc:2093-2129emcauto() 根据参数创建并发送 NML 命令:

  • LOCAL_AUTO_PAUSE -> EMC_TASK_PLAN_PAUSE
  • LOCAL_AUTO_RESUME -> EMC_TASK_PLAN_RESUME
  • LOCAL_AUTO_STEP -> EMC_TASK_PLAN_STEP

关键片段:

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

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

static EMC_TASK_INTERP interpResumeState = EMC_TASK_INTERP::IDLE;

暂停时如果当前不是 PAUSED就保存当前解释器状态恢复时还原

  • 暂停保存:emctaskmain.cc:2337-2343
  • 恢复还原:emctaskmain.cc:2369-2375

PLAN_PAUSE 执行点

emctaskmain.cc:2337-2345

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

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-2624EMC_TASK_EXEC::DONE 分支中只有当 interpState != PAUSED 时才继续从 interp_list 取下一条命令:

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

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-2549STEPPING_CHECK()stepping/steppingWait/steppedLine 控制只执行一步。

这说明 STEP 不是普通 resume也不是永远推进一个 UI sample它是“在 paused 状态下短暂运行,直到 motion id/line 改变后再暂停”。

task 到 motion 的接口

linuxcnc/src/emc/task/taskintf.cc:1417-1422

int emcTrajPause()
{
    emcmotCommand.command = EMCMOT_PAUSE;
    return usrmotWriteEmcmotCommand(&emcmotCommand);
}

taskintf.cc:1438-1449

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

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->pausedmotion.h:631-640 定义,初始化在 motion.c:940-951 清零。

linuxcnc/src/emc/motion/command.c:1234-1240

case EMCMOT_PAUSE:
    tpPause(&emcmotInternal->coord_tp);
    emcmotStatus->paused = 1;
    break;

command.c:1256-1263

case EMCMOT_RESUME:
    emcmotStatus->stepping = 0;
    tpResume(&emcmotInternal->coord_tp);
    emcmotStatus->paused = 0;
    break;

command.c:1265-1274

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

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

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

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

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

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

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-resumeaxis-shell.js:736-748 将其分发为 PAUSE_RESUME,这与 AXIS 工具栏对标方向正确。

Web 当前 isProgramPaused()axis-shell.js:616-620 使用:

state.runState === "paused" ||
state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true

这少了一个 LinuxCNC 真实关键字段motion/traj paused也就是 AXIS 的 s.paused。Web 如果只看 taskPausedinterpState,可能无法准确模拟“运动层已经 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 改为:

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-2624interpState == PAUSED 时不从 interp_list 取下一条。Web 也需要同等约束:所有周期性 playback/status loop 在 paused 时必须只 poll 状态,不推进 sample 或解释器队列。

Web 修正建议

1. 拆清三种状态

建议 Web 状态显式拆成:

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

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

motionPaused = true;
if (interpState !== "paused") interpResumeState = interpState;
interpState = "paused";
taskPaused = true;
runState = "paused";

同时不应修改当前 sampleIndex、当前行、DRO 或 toolhead 位置,只把速度反馈归零。

4. RESUME 动作必须恢复到 interpResumeState

对标 emctaskmain.cc:2369-2375

motionPaused = false;
interpState = interpResumeState || "reading";
taskPaused = false;
singleStepping = false;
runState = interpState === "reading" ? "running" : "idle";

如果 interpResumeStatewaiting,恢复后应回到 waiting再由执行循环完成后转 idle不要一律改成 reading。

5. STEP 不是普通 sample+1

对标 command.c:1265-1274control.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 都要加硬条件:

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_pausedinterp_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 时冻结取样与队列推进。