Files
cnc_wams/项目分析/AXIS主界面按钮调用链与WASM完善建议.md
2026-07-07 16:46:39 -04:00

724 lines
26 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 主界面按钮调用链、状态机制与 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/PythonPython 扩展和 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`
- 切 MDImotion 进 `COORD`abort 当前任务并同步解释器,`mdiOrAuto = MDI`
- 切 AUTOmotion 进 `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 statusWeb 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_RESETMANUALIDLE
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 的乐观赋值。