# AXIS 主界面按钮调用链、状态机制与 WASM 完善建议 分析对象: - 上游源码:`/home/mes123456/cnc_wams/linuxcnc` - 独立 WASM 移植:`/home/mes123456/cnc_wams/wasm-port` - Web 五轴仿真:`/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 本文聚焦 AXIS 主界面的“急停、上电、Home、执行、暂停、单步执行”。LinuxCNC 这里不是纯 C++:AXIS 前端是 Tcl/Python,Python 扩展和 task 主要是 C++,motion/homing 是 C。Web/WASM 移植时应保留这个分层语义,而不是只在 JavaScript 中复刻按钮效果。 ## 1. 总体调用链 AXIS 按钮的完整链路是: ```text AXIS Tcl 按钮/菜单 -> axis.py 回调 -> linuxcnc.command() Python C++ 扩展 -> EMC_* NML 命令写入 emcCommand -> milltask/emctaskmain.cc 周期读取命令 -> emcTaskPlan() 按 task_state/task_mode/interp_state 门控 -> emcTaskIssueCommand() 分发到 task/motion/io -> taskintf.cc 写 EMCMOT_* 到 motion -> motion/command.c 与 homing.c 更新实时运动状态 -> emcTaskUpdate()/emcMotionUpdate() 写 emcStatus -> linuxcnc.stat().poll() -> AXIS Tk 变量 -> axis.tcl trace 调 update_state() 刷新按钮可用性 ``` 关键源码位置: | 层级 | 文件 | 作用 | |---|---|---| | AXIS Tcl UI | `linuxcnc/share/axis/tcl/axis.tcl` | 菜单、工具栏按钮、按钮启用条件、Tk 状态变量 trace | | AXIS Python | `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py` | 按钮回调、`s.poll()` 读状态、`c.*` 发命令 | | Python C++ 扩展 | `linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc` | `linuxcnc.command()` 和 `linuxcnc.stat()` 到 NML 的绑定 | | NML 类型 | `linuxcnc/src/emc/nml_intf/emc.hh` | `EMC_TASK_STATE`、`EMC_TASK_MODE`、`EMC_TASK_INTERP`、命令类型 | | task 主循环 | `linuxcnc/src/emc/task/emctaskmain.cc` | 读命令、门控、解释器计划、执行队列、写 status | | task 状态 | `linuxcnc/src/emc/task/emctask.cc` | `emcTaskSetState()`、`emcTaskSetMode()`、`determineState()`、`emcTaskAbort()` | | task->motion 接口 | `linuxcnc/src/emc/task/taskintf.cc` | 把 task 命令翻译成 `EMCMOT_*` motion 命令 | | motion 命令 | `linuxcnc/src/emc/motion/command.c` | `EMCMOT_ENABLE/PAUSE/RESUME/STEP/JOINT_HOME` 等实际运动状态处理 | | homing 状态机 | `linuxcnc/src/emc/motion/homing.c` | 回零序列和 `homed/homing/allhomed` 状态维护 | ## 2. 状态模型 ### 2.1 task 状态 `emc.hh` 定义: ```text EMC_TASK_STATE: ESTOP, ESTOP_RESET, OFF, ON EMC_TASK_MODE: MANUAL, AUTO, MDI EMC_TASK_INTERP: IDLE, READING, PAUSED, WAITING EMC_TASK_EXEC: DONE, WAITING_FOR_MOTION, WAITING_FOR_IO, ERROR, ... ``` AXIS 显示和按钮门禁主要看: - `s.task_state` - `s.task_mode` - `s.interp_state` - `s.paused` - `s.task_paused` - `s.homed[]` - `s.motion_mode` - `s.file` ### 2.2 task_state 的真实来源 `emcTaskSetState()` 执行的是动作,最终显示的 `task.state` 由 `determineState()` 根据下级状态推导: ```text io.aux.estop == true -> ESTOP io.aux.estop == false 且 motion.traj.enabled == false -> ESTOP_RESET io.aux.estop == false 且 motion.traj.enabled == true -> ON ``` 因此 `STATE_OFF` 在 Python 扩展说明中基本等价于“未使能/急停复位后未上电”语义;真实显示通常落到 `ESTOP_RESET`。Web 端若保留 `off`,应把它作为 UI/过渡状态处理,不能把它理解为能绕过急停复位直接上电的状态。 ### 2.3 task_mode 的真实来源 `determineMode()` 根据 motion 轨迹模式推导: ```text motion.traj.mode == FREE -> MANUAL motion.traj.mode == TELEOP -> MANUAL motion.traj.mode == COORD -> mdiOrAuto ``` `emcTaskSetMode()` 的主要动作: - 切 MANUAL:若全回零则 motion 进 `TELEOP`,否则进 `FREE`。 - 切 MDI:motion 进 `COORD`,abort 当前任务并同步解释器,`mdiOrAuto = MDI`。 - 切 AUTO:motion 进 `COORD`,abort 当前任务并同步解释器,`mdiOrAuto = AUTO`。 ### 2.4 状态如何记录并返回前端 task 主循环每周期执行: ```text emcCommandBuffer->read() emcTaskPlan() emcTaskExecute() emcMotionUpdate(&emcStatus->motion) emcTaskUpdate(&emcStatus->task) emcStatusBuffer->write(emcStatus) ``` AXIS 的 `AxisCanon.update()` 通过 `self.stat.poll()` 读取 status,再写入 Tk 变量: ```text vars.task_mode <- self.stat.task_mode vars.task_state <- self.stat.task_state vars.task_paused <- self.stat.task_paused vars.interp_state <- self.stat.interp_state vars.interp_pause <- self.stat.paused vars.motion_mode <- self.stat.motion_mode ``` `axis.tcl` 对这些变量设置 trace,变量变化后调用 `update_state()`,统一刷新按钮状态。 ## 3. AXIS UI 按钮入口与门禁 `axis.tcl` 中主菜单和工具栏映射: | UI 动作 | Tcl command | Python 函数 | |---|---|---| | F1 / 急停 | `estop_clicked` | `commands.estop_clicked()` | | F2 / 上电下电 | `onoff_clicked` | `commands.onoff_clicked()` | | Run | `task_run` | `commands.task_run()` | | Step | `task_step` | `commands.task_step()` | | Pause | `task_pause` | `commands.task_pause()` | | Resume | `task_resume` | `commands.task_resume()` | | 工具栏 Pause/Resume 合并按钮 | `task_pauseresume` | `commands.task_pauseresume()` | | Home Axis | `home_joint` | `commands.home_joint()` | `update_state()` 的主要按钮可用条件: | 按钮/菜单 | AXIS UI 可执行条件 | |---|---| | 急停 | 始终显示为可按,按下按当前状态切换 ESTOP/ESTOP_RESET | | 上电/下电 | `task_state != STATE_ESTOP` | | Run | `task_state == STATE_ON && interp_state == INTERP_IDLE` | | Run from selected line | `STATE_ON && INTERP_IDLE && highlight_line != -1` | | Home / Unhome / Zero | `task_state == STATE_ON && interp_state == INTERP_IDLE` | | Step | `task_state == STATE_ON && taskfile != ""` | | Pause 菜单 | `STATE_ON && (INTERP_READING || INTERP_WAITING)` | | Resume 菜单 | `STATE_ON && INTERP_PAUSED` | | Pause/Resume 工具栏 | `STATE_ON && interp_state != INTERP_IDLE` | | Stop | `STATE_ON && interp_state != INTERP_IDLE` | 注意:有些 Python 回调本身还有二次检查;因此“UI 可点”和“回调被直接调用后的行为”要分开看。 ## 4. 急停 ### 4.1 调用链 ```text axis.tcl 急停按钮 / F1 -> axis.py estop_clicked() -> s.poll() -> 当前 ESTOP: c.state(STATE_ESTOP_RESET) 否则: c.state(STATE_ESTOP) -> emcmodule.cc state() -> EMC_TASK_SET_STATE -> emctaskmain.cc emcTaskPlan() -> emcTaskIssueCommand() -> emctask.cc emcTaskSetState(ESTOP 或 ESTOP_RESET) ``` ### 4.2 急停的先决条件 - UI 层:急停按钮不依赖上电、模式、程序状态。 - task 层:`EMC_TASK_SET_STATE` 在 ESTOP/OFF/ESTOP_RESET/ON 多状态下都属于可立即处理命令。 - 运动中、解释器读取中、MDI 中均可触发。 ### 4.3 急停执行后的状态记录 `emcTaskSetState(ESTOP)` 主要做: ```text emcMotionAbort() emcSpindleAbort() emcAuxEstopOn() emcTrajDisable() emcCoolantFloodOff() emcTaskAbort() emcIoAbort(TASK_STATE_ESTOP) emcJointUnhome(-2) # 仅 volatile_home 关节 emcAbortCleanup() emcTaskPlanSynch() ``` 随后 `determineState()` 因 `io.aux.estop == true` 返回 `ESTOP`。`emcTaskAbort()` 同时清理: ```text interpState = IDLE execState = DONE task_paused = 0 motionLine/readLine/current command 清零 stepping = 0 steppingWait = 0 interp_list.clear() ``` ### 4.4 解除急停 同一个按钮在当前 `STATE_ESTOP` 时发 `STATE_ESTOP_RESET`。`emcTaskSetState(ESTOP_RESET)`: ```text emcAuxEstopOff() emcCoolantFloodOff() emcTaskAbort() emcIoAbort(TASK_STATE_ESTOP_RESET) emcSpindleAbort() emcAbortCleanup() emcTaskPlanSynch() ``` 此时 motion 仍未 enable,`determineState()` 返回 `ESTOP_RESET`,AXIS 状态显示为 `OFF`。 ## 5. 上电/下电 ### 5.1 调用链 ```text axis.tcl 上电按钮 / F2 -> axis.py onoff_clicked() -> s.poll() -> 当前 STATE_ESTOP_RESET: c.state(STATE_ON) 否则: c.state(STATE_OFF) -> emcmodule.cc state() -> EMC_TASK_SET_STATE -> emctaskmain.cc emcTaskIssueCommand() -> emctask.cc emcTaskSetState() -> taskintf.cc emcTrajEnable()/emcTrajDisable() -> motion/command.c EMCMOT_ENABLE 或 EMCMOT_DISABLE ``` ### 5.2 上电先决条件 - AXIS UI:电源按钮只有 `task_state != STATE_ESTOP` 时可点。 - 要真正进入 ON:当前必须是 `STATE_ESTOP_RESET`。 - 若直接调用 `onoff_clicked()` 且当前不是 `STATE_ESTOP_RESET`,函数会走 `STATE_OFF` 分支。 ### 5.3 上电后的状态记录 `emcTaskSetState(ON)`: ```text emcTrajEnable() emcCoolantFloodOff() ``` motion 收到 `EMCMOT_ENABLE` 后置轨迹/关节使能。下一轮 `determineState()` 看到: ```text io.aux.estop == false motion.traj.enabled == true ``` 于是 `task.state = ON`。AXIS 轮询后更新按钮:Run/Home/MDI/Jog 类动作开始具备进一步门禁条件。 ### 5.4 下电后的状态记录 `emcTaskSetState(OFF)`: ```text emcMotionAbort() emcSpindleAbort() emcTrajDisable() emcIoAbort(TASK_STATE_OFF) emcCoolantFloodOff() emcTaskAbort() emcJointUnhome(-2) emcAbortCleanup() emcTaskPlanSynch() ``` 下一轮 `determineState()` 通常显示 `ESTOP_RESET`,同时解释器和单步状态被清空。配置为 volatile home 的关节会失去 homed。 ## 6. Home ### 6.1 调用链 ```text axis.tcl Home Axis / Home All -> axis.py home_all_joints() 或 home_joint() -> manual_ok() -> ensure_mode(MODE_MANUAL) -> go_home(joint 或 -1) -> set_motion_teleop(0) -> c.home(num) -> emcmodule.cc home() -> EMC_JOINT_HOME -> emctaskmain.cc emcTaskIssueCommand() -> taskintf.cc emcJointHome() -> EMCMOT_JOINT_HOME -> motion/command.c -> homing.c do_home_joint() -> homing.c do_homing() 伺服周期状态机 ``` ### 6.2 Home 先决条件 AXIS/Python 层: - `manual_ok()` 必须为真: - `task_state == STATE_ON` - `interp_state == IDLE`,或 MDI 模式下 MDI 队列仍可接受命令 - `ensure_mode(MODE_MANUAL)` 切到手动模式。 - `go_home()` 会检查所有 joint,若已有 joint 正在 homing,则拒绝。 - 单关节回零时: - 非 identity kinematics 下,从坐标轴字母回零会被拒绝,需用 joint 模式。 - duplicate coordinate letters 下禁止按重复轴字母单独回零。 - 已 homed 的 joint 会弹确认。 task/motion 层: - task 必须处于 ON/MANUAL 才会把 `EMC_JOINT_HOME` 当作可执行命令。 - motion 收到 `EMCMOT_JOINT_HOME` 后要求: - `motion_state == EMCMOT_MOTION_FREE` - `motion.homing-inhibit` 未置位 - 没有其他 homing 正在进行 - motion enable flag 为真 ### 6.3 Home 状态记录 `taskintf.cc emcJointHome()` 只写 motion 命令: ```text emcmotCommand.command = EMCMOT_JOINT_HOME emcmotCommand.joint = joint usrmotWriteEmcmotCommand(&emcmotCommand) ``` 真正状态在 `homing.c`: - `do_home_joint(-1)` 调 `do_home_all()`,并先保证至少一个 joint 为 unhomed。 - 单 joint 调 `do_home_one_joint()`。 - `do_homing()` 每个 servo period 推进 home sequence。 - `HOME_FINISHED` 时 joint `homed=1`、`homing=0`、home state 回到 `HOME_IDLE`。 - `base_get_allhomed()` 遍历所有 active joints,只要一个未 homed 就返回 false。 task 状态回填路径: ```text motion/homing.c H[j].homed -> motion joint_status[j].homed -> taskintf.cc emcJointUpdate() 写 EMC_JOINT_STAT.homed/homing -> emcStatus.motion.joint[j].homed -> emcmodule.cc Stat_homed -> axis.py s.homed -> AXIS 重绘 DRO/按钮 ``` ## 7. 执行 Run ### 7.1 调用链 ```text axis.tcl Run -> axis.py task_run() -> run_warn() -> ensure_mode(MODE_AUTO) -> c.auto(AUTO_RUN, program_start_line) -> emcmodule.cc emcauto() -> EMC_TASK_PLAN_RUN -> emctaskmain.cc emcTaskPlan() -> emcTaskIssueCommand() -> emcTaskPlanOpen() 如需要打开当前 file -> interpState = READING -> emcTaskPlanRead()/emcTaskPlanExecute() -> interp_list 生成 EMC_TRAJ_* 等命令 -> emcTaskExecute() -> taskintf.cc 写 EMCMOT_SET_LINE/SET_CIRCLE 等 motion 命令 -> motion trajectory planner 执行 ``` ### 7.2 Run 先决条件 AXIS UI: - `task_state == STATE_ON` - `interp_state == INTERP_IDLE` - 已有 taskfile Python: - `run_warn()` 通过运行前警告检查。 - `ensure_mode(MODE_AUTO)` 成功。 task: - `EMC_TASK_PLAN_RUN` 在 ON/AUTO/IDLE 下可执行。 - `emcTaskIssueCommand(PLAN_RUN)` 检查: - 若 `!all_homed() && !no_force_homing`,报错 `Can't run a program when not homed`。 - 若文件未打开但 `emcStatus->task.file` 有值,会打开。 ### 7.3 Run 状态记录 Run 入口设置: ```text motion.traj.single_stepping = 0 stepping = 0 steppingWait = 0 programStartLine = run_msg->line task.interpState = READING task.task_paused = 0 ``` 执行中: - `emcTaskPlan()` 读取解释器行,设置 `readLine`、`command`。 - canonical 命令进入 `interp_list`。 - `emcTaskExecute()` 从 `interp_list.get()` 取命令,设置 `currentLine`,并用 `emcTrajSetMotionId(currentLine)` 让 motion 的 id 与程序行关联。 - motion 执行时更新 `motion.traj.id/tag/current_vel/queueFull`。 - `emcTaskUpdate()` 把 `motionLine`、active G/M code、file 等写入 status。 程序结束时,`interpState` 回到 `IDLE`,`execState` 回到 `DONE`,AXIS 的 Run 按钮 relief 恢复。 ## 8. 暂停 Pause 与恢复 Resume ### 8.1 Pause 调用链 ```text axis.tcl Pause 菜单 -> axis.py task_pause() -> 要求 MODE_AUTO 且 interp_state 为 READING/WAITING -> ensure_mode(MODE_AUTO) -> c.auto(AUTO_PAUSE) -> emcmodule.cc emcauto() -> EMC_TASK_PLAN_PAUSE -> emctaskmain.cc emcTaskIssueCommand() -> emcTrajPause() -> taskintf.cc EMCMOT_PAUSE -> motion/command.c tpPause() ``` 工具栏合并按钮 `task_pauseresume()` 逻辑: - task mode 不是 AUTO/MDI:直接返回。 - 若 `s.paused` 为真:未被 `resume-inhibit` 禁止时发 `AUTO_RESUME`。 - 若未暂停且 `interp_state != IDLE`:发 `AUTO_PAUSE`。 ### 8.2 Pause 先决条件 菜单 Pause: - `task_state == STATE_ON` - `task_mode == MODE_AUTO` - `interp_state in (READING, WAITING)` 工具栏 Pause/Resume: - `task_state == STATE_ON` - `task_mode in (AUTO, MDI)` - `interp_state != IDLE` ### 8.3 Pause 状态记录 task 收到 `PLAN_PAUSE`: ```text emcTrajPause() if task.interpState != PAUSED: interpResumeState = task.interpState task.interpState = PAUSED task.task_paused = 1 ``` motion 收到 `EMCMOT_PAUSE`: ```text tpPause(&coord_tp) emcmotStatus->paused = 1 ``` AXIS 同时看: - `interp_state == PAUSED` - `s.paused` / `interp_pause != 0` - `task_paused` 因此 Pause 工具栏按钮 relief 变成 sunken。 ### 8.4 Resume 调用链与状态 ```text axis.py task_resume()/task_pauseresume() -> c.auto(AUTO_RESUME) -> EMC_TASK_PLAN_RESUME -> emcTaskIssueCommand() -> emcTrajResume() -> task.interpState = interpResumeState -> task.task_paused = 0 -> motion.traj.single_stepping = 0 -> stepping = 0 -> steppingWait = 0 -> motion EMCMOT_RESUME -> tpResume() -> emcmotStatus->paused = 0 ``` Resume 先决条件: - Python 要求 `s.paused` 为真。 - task mode 必须是 AUTO 或 MDI。 - 工具栏合并按钮还要检查 `resume_inhibit`,如果 HAL pin 禁止恢复,则忽略。 ## 9. 单步执行 Step ### 9.1 调用链 ```text axis.tcl Step -> axis.py task_step() -> 若不是 AUTO+IDLE,则先 run_warn() -> ensure_mode(MODE_AUTO) -> c.auto(AUTO_STEP) -> emcmodule.cc emcauto() -> EMC_TASK_PLAN_STEP -> emctaskmain.cc 根据 interpState 分支处理 -> task stepping/single_stepping 状态 -> 必要时 emcTrajStep() -> motion/command.c EMCMOT_STEP -> control.c 在 motion id 改变时自动 pause ``` ### 9.2 Step 先决条件 AXIS UI: - `task_state == STATE_ON` - `taskfile != ""` Python/task: - 若当前不是 AUTO+IDLE,`task_step()` 会做运行警告检查。 - `ensure_mode(MODE_AUTO)`。 - task 层仍要求 ON/AUTO 对应状态下允许 `PLAN_STEP`。 - 初次从 IDLE step 时,task 先等价启动 run,再立即 pause。 - 已暂停且 motion 队列中有未执行运动时,`PLAN_STEP` 会调用 `emcTrajStep()` 推进一个 motion id。 ### 9.3 Step 状态记录 task 全局变量: ```text stepping = 1 steppingWait = 0 motion.traj.single_stepping = 1 ``` `STEPPING_CHECK()` 是 `emcTaskExecute()` 中每个等待状态前的宏: - 第一次进入时记录 `steppedLine = currentLine` 并置 `steppingWait = 1`。 - 后续若 `currentLine` 已改变,break,等待下一次 step。 motion 层 `EMCMOT_STEP`: ```text if emcmotStatus->paused: idForStep = emcmotStatus->id emcmotStatus->stepping = 1 tpResume(coord_tp) emcmotStatus->paused = 1 else: reportError("can't STEP while already executing") ``` `control.c` 每周期检测: ```text if emcmotStatus->stepping && idForStep != emcmotStatus->id: tpPause(coord_tp) emcmotStatus->stepping = 0 emcmotStatus->paused = 1 ``` 这说明单步不是简单“跑一行 JS 计时器”,而是 task 的解释器队列单步与 motion 的 trajectory id 单步共同实现。 ## 10. 每个按钮的先决条件与状态影响总表 | 按钮 | UI 先决条件 | task/motion 先决条件 | 发出的核心命令 | 执行后关键状态 | |---|---|---|---|---| | 急停 | 基本始终可触发 | 无需 ON/IDLE | `EMC_TASK_SET_STATE ESTOP` | `io.aux.estop=1`、motion disabled、`task.state=ESTOP`、解释器 abort、volatile home 清除 | | 解除急停 | 当前 ESTOP 时同一按钮执行 reset | 急停链/HAL 允许复位 | `EMC_TASK_SET_STATE ESTOP_RESET` | `io.aux.estop=0`、motion 仍 disabled、`task.state=ESTOP_RESET` | | 上电 | `task_state != ESTOP`;真正上电需当前 `ESTOP_RESET` | motion 可 enable | `EMC_TASK_SET_STATE ON` | `motion.traj.enabled=1`、`task.state=ON` | | 下电 | 电源按钮在非 `ESTOP_RESET` 分支 | 无需 IDLE,属于立即命令 | `EMC_TASK_SET_STATE OFF` | abort、motion disabled、volatile home 清除、通常显示 `ESTOP_RESET` | | Home | `STATE_ON && INTERP_IDLE`;Python 还要求 `manual_ok()` | MANUAL/FREE、motion enabled、无 homing inhibit、无其他 homing | `EMC_JOINT_HOME` -> `EMCMOT_JOINT_HOME` | `joint.N.homing` 过程变化,完成后 `joint.N.homed=1`,`all_homed()` 变真 | | Run | `STATE_ON && INTERP_IDLE && taskfile != ""` | AUTO、已回零或 `no_force_homing`、程序已打开 | `EMC_TASK_PLAN_RUN` | `interpState=READING`、`task_paused=0`、解释器生成 motion 队列 | | Pause | 菜单要求 `STATE_ON && READING/WAITING`;工具栏要求非 IDLE | AUTO,或工具栏允许 AUTO/MDI | `EMC_TASK_PLAN_PAUSE` -> `EMCMOT_PAUSE` | `interpResumeState=原状态`、`interpState=PAUSED`、`task_paused=1`、`motion.paused=1` | | Resume | `STATE_ON && INTERP_PAUSED` 或工具栏 paused | AUTO/MDI、`s.paused`、无 resume inhibit | `EMC_TASK_PLAN_RESUME` -> `EMCMOT_RESUME` | `interpState=interpResumeState`、`task_paused=0`、`motion.paused=0`、单步清零 | | Step | `STATE_ON && taskfile != ""` | AUTO;不同 interpState 分支处理 | `EMC_TASK_PLAN_STEP`,必要时 `EMCMOT_STEP` | `single_stepping=1`、`stepping=1`、执行一个 task/motion id 后回 PAUSED | ## 11. 对 `wasm-port` 的完善建议 `wasm-port` 当前方向正确:它已经有 `runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp`、`linuxcnc_motion_runtime.c`、SDK `runtime/sdk/src/linuxcnc-task-hal.js`,并通过 `lctask_send_command_json()` 接收类似 `EMC_TASK_SET_STATE`、`EMC_TASK_PLAN_RUN/PAUSE/RESUME/STEP` 的命令。 但当前 `linuxcnc_task_hal_wasm.cpp` 仍是“phase4 minimal”运行时: - `TaskRuntime` 用字符串保存 `state/mode/interp_state/exec_state`。 - `EMC_TASK_SET_STATE` 只是直接写 `state` 字符串,尚未按 LinuxCNC 的 `determineState()` 从 IO estop 与 motion enabled 推导。 - `EMC_JOINT_HOME` 直接发一条零位运动并立即认为完成,没有移植 `homing.c` 的 home sequence、`homing_inhibit`、`HOME_*` flags、volatile home。 - `EMC_TASK_PLAN_STEP` 已有 `single_stepping`,但还未完整复刻 `STEPPING_CHECK()` 与 motion id 变化暂停的双层逻辑。 - `nativeTaskReady/nativeHalSyncReady/fullLinuxCncProgramExecutionReady` 仍报告 false,说明它还不是完整 task/motion 语义源。 建议按以下顺序推进: 1. 建立 LinuxCNC task 状态结构镜像 在 WASM runtime 中用枚举替代字符串:`EMC_TASK_STATE`、`EMC_TASK_MODE`、`EMC_TASK_INTERP`、`EMC_TASK_EXEC`。保留对外 JSON 小写/字符串只是 SDK 边界转换。 2. 复刻 `determineState()` 与 `determineMode()` 不要让 `SET_STATE ON` 直接等于 `task.state=ON`。应维护: - `io.aux.estop` - `motion.traj.enabled` - `motion.traj.mode` - `mdiOrAuto` 然后每个 task cycle 推导 `task.state/task.mode`。 3. 把 `emcTaskSetState()` 行为补齐 `ESTOP/OFF/ESTOP_RESET/ON` 至少要更新: - motion abort/enable/disable - spindle/coolant 状态 - interpreter abort/synch - `task_paused/single_stepping/exec_state/interp_state` - volatile home 清除 4. 移植或包装 `homing.c` Home 不能长期用“直接置 allHomed”替代。应把 vendored `motion/homing.c` 与必要 `motion.h/motion_struct.h` 状态结构纳入 WASM,或做最窄 shim 调用: - `set_joint_homing_params()` - `do_home_joint()` - `do_homing()` - `get_allhomed()/get_homed()/get_homing()` 并暴露到 status JSON。 5. 补齐 Step 双层语义 task 层实现 `stepping/steppingWait/steppedLine`,motion 层保留 `idForStep`,当 motion id 改变时 pause。当前 runCycles 中“执行一次后 paused”的做法应替换为 LinuxCNC 的 line/id 驱动。 6. 用 LinuxCNC status 字段作为唯一真实状态源 SDK `readStatus()` 应返回 task/motion status;Web store 只做投影,不再自行判定“已经暂停/已经回零/已经上电”。 7. 增加 native 对照测试 对每个按钮记录 native LinuxCNC 的 status 序列: - command 前状态 - command 后第 1 个 task cycle - motion 若干 servo cycle - status 稳态 WASM 用相同命令序列断言字段一致。 ## 12. 对 `web-rtcp-5axis-xyzbc-trt-sim-plan` 的完善建议 该项目已有 `app/src/state/linuxcnc-task-policy.js` 和 `store.js`,并且已有主按钮对标文档和测试。下一步重点不是继续扩大 JS 语义,而是让 UI 更薄: 1. 保留 `linuxcnc-task-policy.js` 作为 UI 门禁投影 它可以继续计算按钮 disabled 状态,但输入应优先来自 `taskHalRuntime.readStatus()`,而不是 store 自己推导。 2. `store.js` 的按钮动作只发 LinuxCNC 命令 例如: - 急停:发 `{type:"EMC_TASK_SET_STATE", state:"ESTOP"}` - 解除急停:发 `{type:"EMC_TASK_SET_STATE", state:"ESTOP_RESET"}` - 上电:发 `{type:"EMC_TASK_SET_STATE", state:"ON"}` - Home:发 `{type:"EMC_JOINT_HOME", joint:-1}` - Run:发 `{type:"EMC_TASK_PLAN_RUN", line: programStartLine}` - Pause:发 `{type:"EMC_TASK_PLAN_PAUSE"}` - Resume:发 `{type:"EMC_TASK_PLAN_RESUME"}` - Step:发 `{type:"EMC_TASK_PLAN_STEP"}` 然后等待 status loop 回填,不在 reducer 中提前“宣布成功”。 3. 修正 Power 的 UI/回调双语义 AXIS UI 禁用 ESTOP 下电源按钮;但 `onoff_clicked()` 若被直接调用且非 `ESTOP_RESET` 会发 OFF。Web 应: - UI disabled 对标 `task_state == estop`。 - 内部命令测试保留“直接调用 TOGGLE_POWER 时非 estop-reset -> OFF”的回调语义。 4. Home 状态不要直接 `allHomed=true` 先作为过渡兼容可以保留,但应标记为 fallback。正式路径要从 WASM status 的 per-joint `homed[]` 汇总。 5. Pause/Resume 需要同时显示 task 与 motion 暂停 LinuxCNC 中暂停状态由 `task.interpState=PAUSED`、`task.task_paused=1`、`motion.paused=1` 共同组成。Web UI 里的 `runState="paused"` 只能作为显示投影,不应是源状态。 6. Step 测试要覆盖三种状态 - AUTO + IDLE:首次 Step 等价 run from start 后暂停。 - AUTO + READING/WAITING:设置 task stepping,下一条/下一段后暂停。 - AUTO + PAUSED 且 motion queue > 0:发 `EMCMOT_STEP`,motion id 改变后暂停。 7. 证据文件增加字段级 diff 当前 evidence 可继续保留截图和 pass/fail;建议新增 JSON diff: - `task.state/mode/interpState/execState/taskPaused` - `motion.enabled/paused/stepping/id` - `joint[].homed/homing/enabled/inpos` - `currentLine/readLine/motionLine` ## 13. 推荐验收用按钮序列 用于 native LinuxCNC、`wasm-port`、`web-rtcp` 三方对齐: ```text 初始: ESTOP 或 ESTOP_RESET,MANUAL,IDLE 1. F1 reset estop -> ESTOP_RESET, motion disabled 2. F2 power on -> ON, motion enabled 3. Home All -> MANUAL/FREE, homing active, 最终 all_homed=true 4. Run -> AUTO, READING, exec 非 DONE 或 motion queue 活动 5. Pause -> PAUSED, task_paused=1, motion.paused=1 6. Step -> PAUSED 保持,motion id 或 currentLine 推进一个单位 7. Resume -> READING/WAITING, paused 清零 8. F1 estop during run -> ESTOP, abort, disabled, volatile home 清除 ``` 每一步至少记录 command 前、command 后、若干 cycle 后三份 status。若 Web/WASM 只能在 UI 层改变状态而 status 无对应变化,应判定为未完全对标。 ## 14. 结论 LinuxCNC 的主控制按钮不是独立 UI 逻辑,而是围绕 NML command/status、task 周期状态机、motion 实时状态机和 homing 状态机共同实现。移植时最重要的原则是: - UI 按钮只负责发送命令和展示 status。 - task 状态由 IO estop、motion enabled、motion mode 推导。 - Home 状态由 motion/homing.c 的 per-joint 状态机产生。 - Pause/Resume/Step 必须同时维护 task interpreter 状态和 motion trajectory pause/step 状态。 - Web/WASM 中所有 `allHomed/taskPaused/motionPaused/interpState/taskState` 最终都应来自 WASM LinuxCNC runtime status,而不是 JavaScript reducer 的乐观赋值。