# 2026-07-07 AXIS 主控制按钮调用链与 Web 完善指南 ## 1. 文档目标 本文在以下资料基础上整理一个可执行的 Web 项目完善指南: - `/home/mes123456/cnc_wams/linuxcnc` - `/home/mes123456/cnc_wams/wasm-port` - `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` - `/home/mes123456/cnc_wams/项目分析/AXIS主控制按钮调用链与WASM完善指南.md` - `/home/mes123456/cnc_wams/项目分析/AXIS主界面按钮调用链与WASM完善建议.md` 重点覆盖 AXIS 主界面“急停、上电、Home、执行、暂停、单步执行”的调用链、先决条件、状态记录机制,以及 Web 项目后续应如何按 LinuxCNC task/motion 语义继续完善。 ## 2. 总体结论 LinuxCNC AXIS 按钮不是直接修改界面状态,而是走统一控制链: ```text AXIS Tcl/Tk 按钮 -> axis.py 回调 -> linuxcnc.command() Python C++ 扩展 -> EMC_* NML 命令 -> milltask/emctaskmain.cc emcTaskPlan() -> emcTaskIssueCommand() -> emctask.cc task 状态动作 -> taskintf.cc 写 EMCMOT_* motion 命令 -> motion/command.c、control.c、homing.c 更新实时状态 -> emcMotionUpdate()/emcTaskUpdate() 写 emcStatus -> linuxcnc.stat().poll() -> AXIS Tk 变量 trace 刷新按钮可用性 ``` Web 项目必须把状态模型收敛到同一组事实字段,而不是只用 `runState` 或按钮 active 样式: | LinuxCNC 字段 | Web 当前/目标字段 | 用途 | |---|---|---| | `task.state` | `machine.taskState` | `estop`、`estop-reset`、`on` 的主门禁 | | `task.mode` | `machine.mode` | `manual`、`auto`、`mdi` 的命令门禁 | | `task.interpState` | `machine.interpState` | `idle`、`reading`、`paused`、`waiting` | | `task.task_paused` | `machine.taskPaused` | task/interpreter 层暂停 | | `motion.traj.paused` | `machine.motionPaused` | motion/trajectory 层暂停 | | `motion.traj.single_stepping` | 目标新增/统一为 `machine.singleStepping` | 单步执行可见状态 | | `motion.joint[n].homed` | `machine.allHomed` 和目标 `machine.homed[]` | Home/Run 门禁 | | IO estop + motion enabled | `estopActive` + `powerOn` 派生 `taskState` | 对标 `determineState()` | ## 3. AXIS UI 层按钮门禁 AXIS 的按钮可点状态在 `linuxcnc/share/axis/tcl/axis.tcl:update_state()` 集中维护: | 按钮 | AXIS UI 可点条件 | |---|---| | 急停 | 始终可点;当前 ESTOP 时执行解除急停,否则执行急停 | | 上电/下电 | `task_state != STATE_ESTOP`;但真正上电只在 `STATE_ESTOP_RESET` 分支 | | Home/Unhome/Zero | `task_state == STATE_ON && interp_state == INTERP_IDLE` | | Run | `task_state == STATE_ON && interp_state == INTERP_IDLE` | | Step | `task_state == STATE_ON && taskfile != ""` | | 菜单 Pause | `task_state == STATE_ON && interp_state in {READING, WAITING}` | | 菜单 Resume | `task_state == STATE_ON && interp_state == PAUSED` | | 工具栏 Pause/Resume | `task_state == STATE_ON && interp_state != IDLE` | | Stop | `task_state == STATE_ON && interp_state != IDLE` | Python 回调还有二次门禁,例如 `manual_ok()` 要求 `STATE_ON`,并要求解释器空闲或 MDI 队列可接收命令。Web 项目中的 `app/src/state/linuxcnc-task-policy.js` 应继续作为唯一按钮门禁入口,避免 UI 组件各自判断。 ## 4. 急停 ### 4.1 LinuxCNC 调用链 ```text axis.py estop_clicked() -> s.poll() -> 当前 STATE_ESTOP: c.state(STATE_ESTOP_RESET) 否则: c.state(STATE_ESTOP) -> emcmodule.cc state() -> EMC_TASK_SET_STATE -> emctaskmain.cc emcTaskPlan() -> emcTaskIssueCommand() -> emctask.cc emcTaskSetState() ``` `EMC_TASK_SET_STATE` 在 ESTOP、ESTOP_RESET、OFF、ON 多状态下都是 immediate command,可在运动中触发。 ### 4.2 task/motion 状态动作 `emcTaskSetState(ESTOP)` 的关键动作: - `emcMotionAbort()` 中止 motion; - `emcSpindleAbort()` 中止主轴; - `emcAuxEstopOn()` 置 IO 急停; - `emcTrajDisable()` 下发 `EMCMOT_DISABLE`; - `emcTaskAbort()` 清解释器、队列、暂停、单步; - `emcIoAbort(TASK_STATE_ESTOP)` 通知 IO; - `emcJointUnhome(-2)` 清除 `VOLATILE_HOME` joint 的 homed; - `emcTaskPlanSynch()` 同步解释器。 实际 `task.state` 由 `emctask.cc:determineState()` 周期推导: ```text io.aux.estop == true -> ESTOP io.aux.estop == false && motion enabled false -> ESTOP_RESET io.aux.estop == false && motion enabled true -> ON ``` ### 4.3 Web 完善要求 当前 `store.js` 已有 `ESTOP`/`RESET` 分支,但后续应补齐: - 急停时同步清 `machine.motionPaused=false`、`machine.singleStepping=false`、`taskHalPauseLock=null`、`programRuntimeFeedback.currentVelocity=0`。 - 急停时只清 volatile home,而不是无条件清全部 Home;如果 Web 暂无 `VOLATILE_HOME` 配置,需在文档和 evidence 中声明 `xyzbc-trt` 的处理策略。 - `taskState` 不应由 `powerOn` 简单替代,应通过 `deriveTaskState({ estopActive, motionEnabled })` 一处派生。 - Task/HAL runtime 可用时,UI 先进入安全预期状态,但最终以 `readStatus()` 回写为准。 ## 5. 上电/下电 ### 5.1 LinuxCNC 调用链 ```text axis.py onoff_clicked() -> 当前 STATE_ESTOP_RESET: c.state(STATE_ON) 否则: c.state(STATE_OFF) -> emcmodule.cc state() -> EMC_TASK_SET_STATE -> emctask.cc emcTaskSetState(ON/OFF) -> taskintf.cc emcTrajEnable()/emcTrajDisable() -> motion/command.c EMCMOT_ENABLE/EMCMOT_DISABLE ``` 上电不是“非急停即可开机”。AXIS 的 Python 分支要求当前正好是 `STATE_ESTOP_RESET`,否则按钮发送 `STATE_OFF`。 ### 5.2 task/motion 状态动作 `emcTaskSetState(ON)`: - `emcTrajEnable()` 写 `EMCMOT_ENABLE`; - motion 侧要求 HAL `motion.enable` 输入为真,否则报错; - enable 在 motion 控制周期中完成,随后 `determineState()` 才推导出 `STATE_ON`。 `emcTaskSetState(OFF)`: - 中止 motion、主轴、IO; - `emcTrajDisable()`; - `emcTaskAbort()`; - `emcJointUnhome(-2)`; - 同步解释器。 ### 5.3 Web 完善要求 `TOGGLE_POWER` 应严格保持: - 只有 `machine.taskState === "estop-reset"` 时执行 ON; - 当前为 `on` 时执行 OFF; - 当前为 `estop` 时按钮应不可用或返回 `power blocked: reset ESTOP first`,不要把 ESTOP 下的点击伪装成上电。 建议在 `linuxcnc-task-policy.js` 增加 `canPowerToggle` 和 `powerAction`: ```text taskState == estop -> disabled taskState == estop-reset -> action ON taskState == on -> action OFF ``` ## 6. Home ### 6.1 LinuxCNC 调用链 ```text axis.py home_all_joints()/home_joint() -> manual_ok() -> ensure_mode(MODE_MANUAL) -> go_home(-1 或 joint) -> set_motion_teleop(0) -> c.home(joint) -> emcmodule.cc home() -> EMC_JOINT_HOME -> emctaskmain.cc emcTaskIssueCommand() -> taskintf.cc emcJointHome() -> motion/command.c EMCMOT_JOINT_HOME -> homing.c do_home_joint()/do_homing() ``` motion 侧 `EMCMOT_JOINT_HOME` 要求: - 当前 `motion_state == EMCMOT_MOTION_FREE`,即 joint/free mode; - `motion.homing-inhibit` 为 false; - 当前没有 homing 正在执行; - motion enable 为真; - `joint=-1` 表示 Home All。 ### 6.2 状态记录方式 `homing.c` 用每关节 `H[jno]` 记录: - `home_state`:`HOME_IDLE`、`HOME_START`、`HOME_SEARCH_*`、`HOME_FINISHED`、`HOME_ABORT` 等; - `homing`:当前关节正在回零; - `homed`:当前关节已回零; - `homing_active`:全局回零状态。 `control.c` 周期把 `get_homing(joint)`、`get_homed(joint)` 写入 motion status,并更新 HAL `motion.is-all-homed`。 ### 6.3 Web 完善要求 当前 Web 的 `HOME` 已设置 `allHomed` 和 home pose,但还应补齐: - `machine.homed` 数组,长度来自 profile/INI joint 数; - `machine.homing` 和 per-joint `homing` 短暂状态,即使仿真为零速回零,也应经过 `homing -> homed` 的状态事件; - Home All 与单 joint Home 的命令对象应对应 `EMC_JOINT_HOME { joint: -1|n }`; - Home 执行前强制切 `manual`,且 `manualPanel` 回到 manual/joint 语义; - 非 identity kinematics 下按坐标轴 Home 应拒绝,提示使用 joint mode; - 回零过程中禁止 Jog、Run、再次 Home。 ## 7. 执行 Run ### 7.1 LinuxCNC 调用链 ```text axis.py task_run() -> run_warn()/reload_file() -> ensure_mode(MODE_AUTO) -> c.auto(AUTO_RUN, program_start_line) -> emcmodule.cc emcauto() -> EMC_TASK_PLAN_RUN -> emctaskmain.cc emcTaskPlan() -> emcTaskIssueCommand() -> all_homed() 检查 -> emcTaskPlanOpen() -> programStartLine = run_msg->line -> task.interpState = READING -> task.task_paused = 0 -> readahead_reading()/emcTaskExecute() -> canonical motion -> taskintf -> motion queue ``` task 层的关键硬门禁: - 机器必须 `STATE_ON`; - AUTO 模式; - 解释器必须 `IDLE` 才能正常开始新 Run; - 未全部回零且 `no_force_homing` 为 false 时拒绝:`Can't run a program when not homed`; - 程序文件需要已打开,或 `task.file` 可被 `emcTaskPlanOpen()` 打开。 ### 7.2 状态记录方式 Run 后 task 写: - `interpState = READING`; - `task_paused = 0`; - `motion.traj.single_stepping = 0`; - `stepping = 0`、`steppingWait = 0`; - `programStartLine = run_msg->line`。 解释器完成、abort、错误或下级状态变为非 ON 时,再回到 `IDLE` 并清执行状态。 ### 7.3 Web 完善要求 当前 Web 已有 `RUN_FROM_OPERATOR`、`RUN`、Task/HAL status loop、50ms 截图验证。后续要继续收敛: - `RUN` 不应直接由 `runState` 决定,应以 `taskState/mode/interpState/allHomed/programOpen` 为准; - `RUN_FROM_OPERATOR` 可以做引导序列,但最终应仍发 `SET_MODE AUTO` 和 `EMC_TASK_PLAN_RUN`; - `programStartLine` 应统一为 LinuxCNC 行号语义,避免 UI 1-based 与 task `line=0` 混用; - Task/HAL runtime 可用时,应从 `taskHalStatus.ui.activeLine` 和 `motion.program-line` 回写 `activeLine`; - 结束时必须经过 `interpState=idle`、`runState=complete/idle`、`taskPaused=false`。 ## 8. 暂停/恢复 ### 8.1 LinuxCNC 调用链 菜单 Pause: ```text axis.py task_pause() -> 必须 MODE_AUTO 且 interp_state in {READING, WAITING} -> c.auto(AUTO_PAUSE) -> EMC_TASK_PLAN_PAUSE ``` 工具栏 Pause/Resume: ```text axis.py task_pauseresume() -> 必须 MODE_AUTO 或 MODE_MDI -> s.paused 为真: c.auto(AUTO_RESUME) -> 否则 interp_state != IDLE: c.auto(AUTO_PAUSE) ``` task 执行: ```text EMC_TASK_PLAN_PAUSE -> emcTrajPause() -> interpResumeState = 当前 interpState -> task.interpState = PAUSED -> task.task_paused = 1 EMC_TASK_PLAN_RESUME -> emcTrajResume() -> task.interpState = interpResumeState -> task.task_paused = 0 -> motion.traj.single_stepping = 0 -> stepping = 0 ``` motion 侧: ```text EMCMOT_PAUSE -> tpPause(); motion.paused = 1 EMCMOT_RESUME -> tpResume(); motion.paused = 0; motion.stepping = 0 ``` ### 8.2 Web 完善要求 当前 `PAUSE`、`PAUSE_RESUME`、`RESUME` 已基本对标。后续补齐点: - `s.paused` 对应 motion paused,不只是 `interpState=="paused"`; - `taskPaused` 和 `motionPaused` 必须一起出现在状态面板、evidence 和测试断言; - 暂停期间 `programExecutionSampleIndex`、`axisPose`、`toolAxisVector`、Vismach pins 必须冻结; - 恢复时使用 `interpResumeState`,不要固定回 `reading`; - `resume_inhibit` 如果 Web 暂不实现,应在策略状态中显式 `resumeInhibit=false`,并预留 gate。 ## 9. 单步执行 Step ### 9.1 LinuxCNC 调用链 ```text axis.py task_step() -> 如果不是 AUTO 或解释器非 IDLE,先清 highlight 并做 run_warn() -> ensure_mode(MODE_AUTO) -> c.auto(AUTO_STEP) -> emcmodule.cc emcauto() -> EMC_TASK_PLAN_STEP -> emctaskmain.cc emcTaskPlan() ``` task 中分状态处理: - `AUTO + IDLE`:把 Step 转成一次 `EMC_TASK_PLAN_RUN(line=0)`,随后立即 `emcTrajPause()`,`interpState=PAUSED`,`task_paused=1`; - `AUTO + READING/WAITING`:设置 `motion.traj.single_stepping=1`、`stepping=1`、`steppingWait=0`; - `AUTO + PAUSED`:若 motion queue 中有暂停运动,发 `emcTrajStep()`;否则恢复解释器到 `interpResumeState` 以读下一步。 motion 侧: ```text EMCMOT_STEP -> 如果 motion.paused: idForStep = current id motion.stepping = 1 tpResume() motion.paused = 1 否则报错 "can't STEP while already executing" control.c 周期: if stepping && idForStep != current id: tpPause() stepping = 0 paused = 1 ``` ### 9.2 Web 完善要求 当前 `STEP` 会推进一个 sample 并设 paused。后续应改成更接近 LinuxCNC 的两层语义: - 增加 `machine.singleStepping` 与 `machine.motionStepping`; - 第一次 Step 在 IDLE 时等价于启动 Auto Run 后暂停; - 已暂停且 motion queue 有待执行段时,Step 只释放到下一个 motion id/line,而不是简单固定一个 sample; - Step 后状态应为 `interpState=paused`、`taskPaused=true`、`motionPaused=true`、`singleStepping=true/随后 false`; - Resume 必须清 `singleStepping`。 ## 10. WASM-port 完善方向 `wasm-port/SKILL.md` 的原则是:CNC 语义来自 LinuxCNC vendored source,JS/SDK 只做适配。针对本按钮状态机,建议按以下顺序补齐: 1. 在 `wasm-port` 的 task/HAL SDK 中暴露完整 task 状态: - `state`、`mode`、`interpState`、`interpResumeState`、`taskPaused`、`singleStepping`、`programOpen`、`programStartLine`。 2. 暴露 motion 状态: - `enabled`、`paused`、`stepping`、`idForStep`、`queueDepth`、`programLine`、`homing`、`homed[]`、`allHomed`。 3. 将 `EMC_TASK_SET_STATE`、`EMC_TASK_SET_MODE`、`EMC_JOINT_HOME`、`EMC_TASK_PLAN_RUN/PAUSE/RESUME/STEP` 作为 JSON command 的稳定 ABI。 4. 对照 LinuxCNC `emcTaskSetState()` 和 `emcTaskPlan()` 增加任务状态矩阵测试。 5. 对照 `motion/command.c` 增加 pause/resume/step motion id 测试。 6. 对照 `homing.c` 增加 Home All、重复 Home、homing-inhibit、正在 homing 时拒绝再次 Home 的测试。 Web 项目不要在 JS 中重新实现解释器、G-code、planner 语义;可以做 UI 预期状态,但最终要以 Task/HAL WASM status 回写为准。 ## 11. web-rtcp 项目落地清单 ### 11.1 应修改/复核的核心文件 | 文件 | 后续职责 | |---|---| | `app/src/state/linuxcnc-task-policy.js` | 唯一按钮门禁;补 `canPowerToggle`、`canStepStrict`、`resumeInhibit`、`singleStepping` | | `app/src/state/store.js` | 按 EMC 命令语义更新状态;清理只依赖 `runState` 的残留判断 | | `app/src/runtime/linuxcnc-task-hal-runtime.js` | 标准化 Task/HAL status,补 `enabled`、`homed[]`、`singleStepping` | | `app/src/ui/axis-shell.js` | AXIS 按钮可用性只读 policy,不散落条件 | | `tests/node/verify_xyzbc_trt_web_app.mjs` | 补逐按钮非法状态矩阵 | | `tests/node/verify_rtcp_store.mjs` | 补状态记录和派生状态断言 | | `tools/verify-estop-power-home-run-pause-50ms.mjs` | 扩展 Step、Home 过程状态和 motion freeze 断言 | | `tools/collect-web-xyzbc-trt-evidence.mjs` | evidence 输出按钮状态矩阵和状态流 | | `tools/compare-xyzbc-trt-evidence.mjs` | 对比 native/Web 的按钮先决条件、状态转移和暂停冻结 | ### 11.2 建议新增状态对象 ```js machine: { taskState: "estop" | "estop-reset" | "on", mode: "manual" | "auto" | "mdi", interpState: "idle" | "reading" | "paused" | "waiting", interpResumeState: "idle" | "reading" | "waiting", taskPaused: boolean, motionPaused: boolean, singleStepping: boolean, motionStepping: boolean, motionEnabled: boolean, homing: boolean, homed: boolean[], allHomed: boolean, noForceHoming: boolean, resumeInhibit: boolean } ``` ### 11.3 验收矩阵 | 场景 | 期望 | |---|---| | ESTOP 下点 Power | 不上电,提示先解除急停 | | ESTOP_RESET 下点 Power | `taskState=on`、`motionEnabled=true` | | ON 下点 Power | abort、disable、`taskState=estop-reset` | | 未上电 Home | 拒绝 | | AUTO/READING Home | 拒绝 | | MANUAL/ON/IDLE Home All | 进入 homing,完成后 `homed[]` 全 true | | 未 Home Run | 拒绝,除非 `noForceHoming=true` | | ON/AUTO/IDLE Run | `interpState=reading`、`taskPaused=false` | | AUTO/READING 菜单 Pause | `interpState=paused`、`taskPaused=true`、`motionPaused=true` | | MANUAL 残留 running 点 Pause | 拒绝 | | 工具栏 paused 点 Pause/Resume | Resume,恢复到 `interpResumeState` | | IDLE Step | 等价启动 Run 后暂停 | | PAUSED Step | 只推进到下一个 motion id/line 后再次暂停 | | 急停 during Run | abort,清 pause/step,velocity=0,状态 ESTOP | ## 12. 推荐实施顺序 1. 先补 `linuxcnc-task-policy.js` 的严格状态矩阵,不改 UI。 2. 补 `store.js` 的 `deriveTaskState()`、`singleStepping`、`homed[]`、急停/下电清理。 3. 补 Task/HAL status normalization,使 WASM status 能覆盖 JS 预期状态。 4. 扩展 Node 测试覆盖非法状态矩阵。 5. 扩展 50ms Playwright 工具,加入 Step 和 Home 过程截图/evidence。 6. 更新 `collect-web` 与 `compare`,把按钮状态流纳入 `60/60` 之外的硬检查。 ## 13. 当前判断 现有 Web 项目已经具备主干:`linuxcnc-task-policy.js`、`store.js`、Task/HAL runtime、50ms 截图验证和 native/Web compare。下一阶段的重点不是新增按钮,而是把按钮行为的状态来源从“Web 自定义运行态”进一步收敛到 LinuxCNC 的 `task.state + task.mode + interpState + motion status + homing status`,并让所有测试和 evidence 都能证明这一点。 ## 14. 2026-07-07 实施完成记录 截至 2026-07-07 18:18 EDT,本指南中的 Web 完善清单已完成一轮落地: - `linuxcnc-task-policy.js` 已作为主控制按钮唯一门禁入口,新增 `motionEnabled`、`homed[]`、`singleStepping`、`motionStepping`、`resumeInhibit`、`canPowerToggle`、`powerAction` 等状态输出。 - `store.js` 已把 `ESTOP/RESET/Power/Home/Run/Pause/Resume/Step` 收敛到 `taskState + mode + interpState + motion/home status`,并清理下电、急停、暂停、单步、回零的状态残留。 - Task/HAL WASM 已输出 `motionEnabled`、`homed[]`,并在 `EMC_TASK_PLAN_RUN` 中执行 ON、AUTO、IDLE、非 homing、已 Home、程序打开、motion plan 已加载的硬门禁。 - Home All 已具备 `homing -> homed` 瞬态记录;Run/Step/Pause/Resume 的 task/motion 状态与 Web policy、status loop 和 evidence 采集同步。 - `collect-web-xyzbc-trt-evidence.mjs` 已改为合法 Task/HAL 运行准备序列:`ON -> HOME -> cycle -> AUTO -> PLAN_RUN`。 本轮验证结果: ```text linuxcnc_task_hal_wasm_build=ok linuxcnc_task_runtime_smoke=ok task_state_matrix=ok linuxcnc_task_hal_sdk=ok xyzbc_trt_web_app_smoke=ok xyzbc_trt_browser_smoke=ok gmoccapy_static_build=ok compare_xyzbc_trt_status=pass compare.summary.checkCount=60 compare.summary.passCount=60 compare.summary.failCount=0 compare.summary.blockers=[] compare.requiredImprovements=[] ``` 结论:本指南从“建议实施”转为“已实施并通过复验”。后续若继续扩展截图工具或 compare 检查,应在当前 T-078 基线之上追加新任务,而不是重新发散按钮状态来源。