docs: add axis run process working set
This commit is contained in:
929
项目分析/AXIS主控制按钮调用链与WASM完善指南.md
Normal file
929
项目分析/AXIS主控制按钮调用链与WASM完善指南.md
Normal file
@@ -0,0 +1,929 @@
|
||||
# AXIS 主控制按钮调用链与 WASM 完善指南
|
||||
|
||||
生成时间:2026-07-07
|
||||
|
||||
分析范围:
|
||||
|
||||
- LinuxCNC 源码:`/home/mes123456/cnc_wams/linuxcnc`
|
||||
- WASM 迁移工程:`/home/mes123456/cnc_wams/wasm-port`
|
||||
- Web 5 轴仿真工程:`/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan`
|
||||
|
||||
本文重点分析 AXIS 主界面的“急停、上电、Home、执行、暂停、单步执行”的调用链、状态机制、按钮前置条件,以及对 WASM/Web 项目的完善建议。
|
||||
|
||||
## 1. 总体架构结论
|
||||
|
||||
LinuxCNC 中 AXIS 按钮不是直接操作运动控制,而是走统一命令/状态通道:
|
||||
|
||||
```text
|
||||
AXIS Python/Tk 按钮
|
||||
-> linuxcnc.command() Python C 扩展
|
||||
-> NML 命令对象 EMC_TASK_*/EMC_JOINT_*
|
||||
-> emcCommand NML 通道
|
||||
-> task 主循环 emcTaskPlan()/emcTaskIssueCommand()
|
||||
-> taskintf.cc 转为 EMCMOT_* motion 命令
|
||||
-> motion 实时侧 command.c/control.c/homing.c
|
||||
-> motion/status + task/status 合成为 emcStatus
|
||||
-> linuxcnc.stat().poll() 供 AXIS 刷新 UI
|
||||
```
|
||||
|
||||
核心状态不是由按钮自行保存,而是由 `emcStatus` 聚合:
|
||||
|
||||
- `emcStatus->task.state`:任务状态,GUI 显示为 `STATE_ESTOP`、`STATE_ESTOP_RESET`、`STATE_ON` 等。
|
||||
- `emcStatus->task.mode`:任务模式,`MODE_MANUAL`、`MODE_AUTO`、`MODE_MDI`。
|
||||
- `emcStatus->task.interpState`:解释器状态,`INTERP_IDLE`、`INTERP_READING`、`INTERP_PAUSED`、`INTERP_WAITING`。
|
||||
- `emcStatus->task.task_paused`:task 层暂停标志。
|
||||
- `emcStatus->motion.traj.paused`:motion 轨迹规划器暂停标志。
|
||||
- `emcStatus->motion.traj.single_stepping`:task/GUI 可见的单步执行标志。
|
||||
- `emcStatus->motion.joint[n].homed`:每个 joint 的回零状态。
|
||||
- `emcStatus->motion.traj.enabled`:motion 是否上使能,用于推导 task 是否 `STATE_ON`。
|
||||
|
||||
需要特别注意:LinuxCNC 的 task 层主要是 C++,motion 层主要是 C。用户所说的 C++ 实现,在源码中实际跨越 `emctask*.cc`、`taskintf.cc` 和 motion 的 `command.c`、`homing.c`。
|
||||
|
||||
## 2. AXIS UI 按钮入口
|
||||
|
||||
AXIS 主界面位于:
|
||||
|
||||
- `linuxcnc/bin/axis`
|
||||
|
||||
关键辅助函数:
|
||||
|
||||
```python
|
||||
def manual_ok(do_poll=True):
|
||||
if do_poll: s.poll()
|
||||
if s.task_state != linuxcnc.STATE_ON: return False
|
||||
return s.interp_state == linuxcnc.INTERP_IDLE or (
|
||||
s.task_mode == linuxcnc.MODE_MDI and
|
||||
s.queued_mdi_commands < vars.max_queued_mdi_commands.get()
|
||||
)
|
||||
|
||||
def ensure_mode(m, *p):
|
||||
s.poll()
|
||||
if s.task_mode == m or s.task_mode in p: return True
|
||||
c.mode(m)
|
||||
c.wait_complete()
|
||||
s.poll()
|
||||
return True
|
||||
```
|
||||
|
||||
源码位置:`linuxcnc/bin/axis:986-1015`。
|
||||
|
||||
这说明 AXIS 侧前置条件分两层:
|
||||
|
||||
1. UI 层用 `s.poll()` 读当前状态,避免明显非法操作。
|
||||
2. task/motion 层仍会再次校验,非法命令会拒绝或报错。
|
||||
|
||||
## 3. Python C 扩展到 NML 命令
|
||||
|
||||
AXIS 使用 `linuxcnc.command()`。该对象由 `linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc` 暴露,关键映射如下:
|
||||
|
||||
| AXIS 调用 | C 扩展函数 | NML 命令 |
|
||||
| --- | --- | --- |
|
||||
| `c.state(STATE_*)` | `state()` | `EMC_TASK_SET_STATE` |
|
||||
| `c.mode(MODE_*)` | `mode()` | `EMC_TASK_SET_MODE` |
|
||||
| `c.home(joint)` | `home()` | `EMC_JOINT_HOME` |
|
||||
| `c.auto(AUTO_RUN, line)` | `emcauto()` | `EMC_TASK_PLAN_RUN` |
|
||||
| `c.auto(AUTO_PAUSE)` | `emcauto()` | `EMC_TASK_PLAN_PAUSE` |
|
||||
| `c.auto(AUTO_RESUME)` | `emcauto()` | `EMC_TASK_PLAN_RESUME` |
|
||||
| `c.auto(AUTO_STEP)` | `emcauto()` | `EMC_TASK_PLAN_STEP` |
|
||||
|
||||
源码依据:
|
||||
|
||||
- `mode()` 创建 `EMC_TASK_SET_MODE`:`linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc:1691-1705`
|
||||
- `state()` 创建 `EMC_TASK_SET_STATE`:`linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc:1815-1829`
|
||||
- `home()` 创建 `EMC_JOINT_HOME`:`linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc:1941-1945`
|
||||
- `emcauto()` 分发 `RUN/PAUSE/RESUME/STEP`:`linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc:2093-2116`
|
||||
|
||||
## 4. task 主循环的命令调度
|
||||
|
||||
task 主循环在 `linuxcnc/src/emc/task/emctaskmain.cc`:
|
||||
|
||||
```text
|
||||
emcCommandBuffer->read()
|
||||
-> emcTaskPlan()
|
||||
-> emcTaskExecute()
|
||||
-> emcMotionUpdate(&emcStatus->motion)
|
||||
-> emcTaskUpdate(&emcStatus->task)
|
||||
-> 写 emcStatus 顶层 command/status/echo_serial_number
|
||||
```
|
||||
|
||||
源码位置:`linuxcnc/src/emc/task/emctaskmain.cc:3301-3478`。
|
||||
|
||||
`emcTaskPlan()` 按三类状态进行门禁:
|
||||
|
||||
1. `task.state`:`OFF`、`ESTOP`、`ESTOP_RESET`、`ON`
|
||||
2. `task.mode`:`MANUAL`、`AUTO`、`MDI`
|
||||
3. `task.interpState`:`IDLE`、`READING`、`PAUSED`、`WAITING`
|
||||
|
||||
因此,AXIS UI 的按钮条件必须与 `emcTaskPlan()` 的状态表一致,不能只看 `runState` 这类 Web 自定义状态。
|
||||
|
||||
## 5. 急停按钮
|
||||
|
||||
### 5.1 AXIS UI 调用链
|
||||
|
||||
源码:`linuxcnc/bin/axis:2243-2248`
|
||||
|
||||
```python
|
||||
def estop_clicked(event=None):
|
||||
s.poll()
|
||||
if s.task_state == linuxcnc.STATE_ESTOP:
|
||||
c.state(linuxcnc.STATE_ESTOP_RESET)
|
||||
else:
|
||||
c.state(linuxcnc.STATE_ESTOP)
|
||||
```
|
||||
|
||||
按钮语义:
|
||||
|
||||
- 当前是 `STATE_ESTOP`:发送 `STATE_ESTOP_RESET`,表示解除急停。
|
||||
- 其他任意状态:发送 `STATE_ESTOP`,进入急停。
|
||||
|
||||
### 5.2 NML/task 调用链
|
||||
|
||||
```text
|
||||
c.state(STATE_ESTOP or STATE_ESTOP_RESET)
|
||||
-> emcmodule.cc state()
|
||||
-> EMC_TASK_SET_STATE
|
||||
-> emcTaskPlan()
|
||||
-> emcTaskIssueCommand()
|
||||
-> emcTaskSetState()
|
||||
```
|
||||
|
||||
`EMC_TASK_SET_STATE` 被 task 作为 immediate command 接收:
|
||||
|
||||
- 非 ON 状态也允许接收:`emctaskmain.cc:795-829`
|
||||
- ON/MANUAL 允许:`emctaskmain.cc:897-956`
|
||||
- ON/AUTO/IDLE 允许:`emctaskmain.cc:1026-1075`
|
||||
- ON/AUTO/READING 允许:`emctaskmain.cc:1137-1174`
|
||||
- ON/AUTO/PAUSED 允许:`emctaskmain.cc:1206-1251`
|
||||
|
||||
### 5.3 C++ task 状态处理
|
||||
|
||||
`emcTaskSetState(ESTOP)` 源码:`linuxcnc/src/emc/task/emctask.cc:353-365`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `emcMotionAbort()`:中止 motion。
|
||||
- `emcSpindleAbort()`:中止主轴。
|
||||
- `emcAuxEstopOn()`:IO 急停输出置位。
|
||||
- `emcTrajDisable()`:下发 motion disable。
|
||||
- 关闭 flood coolant。
|
||||
- `emcTaskAbort()`:清解释器队列、清 pause/step 状态。
|
||||
- `emcIoAbort(TASK_STATE_ESTOP)`。
|
||||
- `emcJointUnhome(-2)`:清除 `VOLATILE_HOME` joint 的 homed。
|
||||
- `emcTaskPlanSynch()`:同步解释器。
|
||||
|
||||
`emcTaskSetState(ESTOP_RESET)` 源码:`linuxcnc/src/emc/task/emctask.cc:342-350`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `emcAuxEstopOff()`:解除 IO 急停。
|
||||
- `emcTaskAbort()`:清程序运行状态。
|
||||
- `emcIoAbort(TASK_STATE_ESTOP_RESET)`。
|
||||
- `emcSpindleAbort()`。
|
||||
- `emcTaskPlanSynch()`。
|
||||
|
||||
### 5.4 状态记录方式
|
||||
|
||||
task 并不简单把 `task.state = ESTOP` 固定写死,而是周期性通过 `determineState()` 从 IO 和 motion 推导:
|
||||
|
||||
源码:`linuxcnc/src/emc/task/emctask.cc:418-428`
|
||||
|
||||
规则:
|
||||
|
||||
| IO 急停 | motion enabled | 推导状态 |
|
||||
| --- | --- | --- |
|
||||
| true | 任意 | `ESTOP` |
|
||||
| false | false | `ESTOP_RESET` |
|
||||
| false | true | `ON` |
|
||||
|
||||
`emcTaskUpdate()` 每周期写入 `stat->state`:`linuxcnc/src/emc/task/emctask.cc:688-699`。
|
||||
|
||||
这意味着 Web/WASM 中不能只维护一个 `estopActive` 布尔值;应该同时维护:
|
||||
|
||||
- IO estop 状态;
|
||||
- motion enable 状态;
|
||||
- task 推导状态;
|
||||
- volatile home 清除动作。
|
||||
|
||||
## 6. 上电/下电按钮
|
||||
|
||||
### 6.1 AXIS UI 调用链
|
||||
|
||||
源码:`linuxcnc/bin/axis:2250-2260`
|
||||
|
||||
```python
|
||||
def onoff_clicked(event=None):
|
||||
s.poll()
|
||||
if s.task_state == linuxcnc.STATE_ESTOP_RESET:
|
||||
c.state(linuxcnc.STATE_ON)
|
||||
...
|
||||
else:
|
||||
c.state(linuxcnc.STATE_OFF)
|
||||
```
|
||||
|
||||
按钮语义:
|
||||
|
||||
- 只有 `STATE_ESTOP_RESET` 才能上电。
|
||||
- 其他状态一律发送 `STATE_OFF`。
|
||||
- 因此 `STATE_ESTOP` 下点电源不是“上电”,而是进入 OFF/保持不可上电语义;必须先通过急停按钮解除急停到 `ESTOP_RESET`。
|
||||
|
||||
### 6.2 task 调用链
|
||||
|
||||
```text
|
||||
c.state(STATE_ON/OFF)
|
||||
-> EMC_TASK_SET_STATE
|
||||
-> emcTaskSetState(ON/OFF)
|
||||
```
|
||||
|
||||
`emcTaskSetState(ON)` 源码:`linuxcnc/src/emc/task/emctask.cc:336-340`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `emcTrajEnable()`。
|
||||
- 关闭 flood coolant。
|
||||
|
||||
`emcTrajEnable()` 源码:`linuxcnc/src/emc/task/taskintf.cc:1396-1400`
|
||||
|
||||
```text
|
||||
emcTrajEnable()
|
||||
-> emcmotCommand.command = EMCMOT_ENABLE
|
||||
-> usrmotWriteEmcmotCommand()
|
||||
```
|
||||
|
||||
motion 侧 `EMCMOT_ENABLE` 源码:`linuxcnc/src/emc/motion/command.c:1370-1384`
|
||||
|
||||
执行内容:
|
||||
|
||||
- 如果 HAL `motion.enable` 输入为 false,报错:`can't enable motion, enable input is false`。
|
||||
- 否则设置 `emcmotInternal->enabling = 1`,实际使能在 motion 控制周期中完成。
|
||||
|
||||
`emcTaskSetState(OFF)` 源码:`linuxcnc/src/emc/task/emctask.cc:323-333`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `emcMotionAbort()`。
|
||||
- `emcSpindleAbort()`。
|
||||
- `emcTrajDisable()`。
|
||||
- `emcIoAbort(TASK_STATE_OFF)`。
|
||||
- `emcTaskAbort()`。
|
||||
- `emcJointUnhome(-2)` 清 volatile home。
|
||||
- `emcTaskPlanSynch()`。
|
||||
|
||||
motion 侧 `EMCMOT_DISABLE` 源码:`linuxcnc/src/emc/motion/command.c:1357-1368`
|
||||
|
||||
执行内容:
|
||||
|
||||
- 设置 `emcmotInternal->enabling = 0`。
|
||||
- 对 inverse-only kinematics,还会退出 teleop/coord。
|
||||
|
||||
### 6.3 状态记录方式
|
||||
|
||||
上电后并不是由 task 直接写 `STATE_ON`,而是 motion 报告 enabled 后,`determineState()` 推导为 `ON`。
|
||||
|
||||
motion 状态更新路径:
|
||||
|
||||
```text
|
||||
motion command/control 周期更新 emcmotStatus.motionFlag
|
||||
-> taskintf.cc emcMotionUpdate()
|
||||
-> emcTrajUpdate()
|
||||
-> stat->traj.enabled
|
||||
-> emcTaskUpdate()
|
||||
-> determineState()
|
||||
```
|
||||
|
||||
关键源码:
|
||||
|
||||
- `emcMotionUpdate()` 读取 `emcmotStatus`:`linuxcnc/src/emc/task/taskintf.cc:2057-2102`
|
||||
- `emcTrajUpdate()` 写 `stat->enabled`:`linuxcnc/src/emc/task/taskintf.cc:1639-1652`
|
||||
- `determineState()` 推导状态:`linuxcnc/src/emc/task/emctask.cc:418-428`
|
||||
|
||||
## 7. Home 按钮
|
||||
|
||||
### 7.1 AXIS UI 调用链
|
||||
|
||||
Home All 源码:`linuxcnc/bin/axis:2732-2740`
|
||||
|
||||
```python
|
||||
def home_all_joints(event=None):
|
||||
if not manual_ok(): return
|
||||
ensure_mode(linuxcnc.MODE_MANUAL)
|
||||
...
|
||||
go_home(-1)
|
||||
```
|
||||
|
||||
单 joint Home 源码:`linuxcnc/bin/axis:2747-2767`
|
||||
|
||||
关键条件:
|
||||
|
||||
- `manual_ok()` 必须为真,即 machine 为 `STATE_ON`,解释器 idle 或 MDI 队列可接受。
|
||||
- 切到 `MODE_MANUAL`。
|
||||
- 已 homed 时弹窗确认是否 re-home。
|
||||
- 非 identity kinematics 下,单轴/单 joint 选择有额外限制。
|
||||
- 最终 `go_home(num)` 调 `c.home(num)`。
|
||||
|
||||
### 7.2 NML/task/motion 调用链
|
||||
|
||||
```text
|
||||
c.home(-1 或 joint)
|
||||
-> emcmodule.cc home()
|
||||
-> EMC_JOINT_HOME
|
||||
-> emcTaskPlan()
|
||||
-> emcTaskIssueCommand()
|
||||
-> emcJointHome()
|
||||
-> EMCMOT_JOINT_HOME
|
||||
-> motion command.c
|
||||
-> do_home_joint()
|
||||
-> homing.c 状态机
|
||||
```
|
||||
|
||||
`EMC_JOINT_HOME` 只在 ON/MANUAL 中直接允许:`linuxcnc/src/emc/task/emctaskmain.cc:897-956`。
|
||||
|
||||
`emcJointHome()` 源码:`linuxcnc/src/emc/task/taskintf.cc:796-805`
|
||||
|
||||
执行内容:
|
||||
|
||||
- 检查 joint 范围。
|
||||
- 下发 `EMCMOT_JOINT_HOME`。
|
||||
|
||||
motion 侧 `EMCMOT_JOINT_HOME` 源码:`linuxcnc/src/emc/motion/command.c:1411-1439`
|
||||
|
||||
motion 前置条件:
|
||||
|
||||
- `emcmotStatus->motion_state == EMCMOT_MOTION_FREE`,必须处于 joint/free 模式。
|
||||
- `motion.homing-inhibit` 不能为 true。
|
||||
- 当前不能已有 homing 流程在跑。
|
||||
- `GET_MOTION_ENABLE_FLAG()` 必须为 true。
|
||||
- 负 joint 表示 home all。
|
||||
|
||||
### 7.3 homing 状态机
|
||||
|
||||
`do_home_joint()` 最终进入 `linuxcnc/src/emc/motion/homing.c`。
|
||||
|
||||
核心状态记录:
|
||||
|
||||
- `H[joint].homing`
|
||||
- `H[joint].homed`
|
||||
- `H[joint].home_state`
|
||||
- `H[joint].joint_in_sequence`
|
||||
- `joint.free_tp.enable`
|
||||
|
||||
输出 HAL pin:
|
||||
|
||||
源码:`linuxcnc/src/emc/motion/homing.c:541-573`
|
||||
|
||||
- `joint.N.homing`
|
||||
- `joint.N.homed`
|
||||
- `joint.N.home-state`
|
||||
- `joint.N.index-enable`
|
||||
|
||||
完成逻辑:
|
||||
|
||||
源码:`linuxcnc/src/emc/motion/homing.c:1376-1385`
|
||||
|
||||
- `homing = 0`
|
||||
- `homed = 1`
|
||||
- `home_state = HOME_IDLE`
|
||||
- `joint_in_sequence = 0`
|
||||
|
||||
异常中止:
|
||||
|
||||
源码:`linuxcnc/src/emc/motion/homing.c:1387-1397`
|
||||
|
||||
- 所有 joint `homing = 0`
|
||||
- 所有 joint `homed = 0`
|
||||
- 所有 `free_tp.enable = 0`
|
||||
- `home_state = HOME_IDLE`
|
||||
|
||||
### 7.4 状态记录方式
|
||||
|
||||
task 通过 `emcMotionUpdate()` 将 motion homed 状态复制到 `emcStatus->motion.joint[n].homed`,AXIS 的 `s.homed[n]` 来自这里。
|
||||
|
||||
`task` 的 `all_homed()` 直接查 `emcStatus->motion.joint[i].homed`,源码:`linuxcnc/src/emc/task/emctaskmain.cc:151-152`。
|
||||
|
||||
## 8. 执行 Run 按钮
|
||||
|
||||
### 8.1 AXIS UI 调用链
|
||||
|
||||
源码:`linuxcnc/bin/axis:2379-2393`
|
||||
|
||||
```python
|
||||
def task_run(*event):
|
||||
res = run_warn()
|
||||
...
|
||||
ensure_mode(linuxcnc.MODE_AUTO)
|
||||
c.auto(linuxcnc.AUTO_RUN, program_start_line)
|
||||
program_start_line = 0
|
||||
```
|
||||
|
||||
AXIS UI 层条件:
|
||||
|
||||
- 会执行 `run_warn()`,处理文件变更、保存、运行前警告等。
|
||||
- `ensure_mode(MODE_AUTO)` 切入 AUTO。
|
||||
- 然后发送 `AUTO_RUN`。
|
||||
|
||||
真正硬条件在 task 层:
|
||||
|
||||
- machine 必须 ON。
|
||||
- AUTO 模式下允许。
|
||||
- 未 homed 且没有 `no_force_homing` 时拒绝。
|
||||
- 程序文件应已打开;未打开时会尝试打开 `emcStatus->task.file`。
|
||||
|
||||
### 8.2 task 调用链
|
||||
|
||||
```text
|
||||
c.auto(AUTO_RUN, line)
|
||||
-> EMC_TASK_PLAN_RUN
|
||||
-> emcTaskPlan() 按状态门禁
|
||||
-> emcTaskIssueCommand()
|
||||
-> case EMC_TASK_PLAN_RUN
|
||||
```
|
||||
|
||||
`EMC_TASK_PLAN_RUN` 在 ON/AUTO/IDLE 中允许:`linuxcnc/src/emc/task/emctaskmain.cc:1015-1075`。
|
||||
|
||||
核心处理源码:`linuxcnc/src/emc/task/emctaskmain.cc:2318-2335`
|
||||
|
||||
执行内容:
|
||||
|
||||
- 如果未 homed 且 `!no_force_homing`,报错:`Can't run a program when not homed`。
|
||||
- 清 `motion.traj.single_stepping`。
|
||||
- 清 `stepping`、`steppingWait`。
|
||||
- 必要时 `emcTaskPlanOpen(emcStatus->task.file)`。
|
||||
- 记录 `programStartLine = run_msg->line`。
|
||||
- 设置 `emcStatus->task.interpState = READING`。
|
||||
- 设置 `emcStatus->task.task_paused = 0`。
|
||||
|
||||
### 8.3 解释器和 motion 下发
|
||||
|
||||
Run 后 `emcTaskPlan()` 进入 `READING`,周期性调用 readahead 逻辑读取 G-code。解释器通过 canonical 函数把运动命令排入 `interp_list`,`emcTaskExecute()` 再把 motion 命令下发到 motion。
|
||||
|
||||
运动命令如直线/圆弧最终在 motion `command.c` 中以 `EMCMOT_SET_LINE`、`EMCMOT_SET_CIRCLE` 等处理。运动状态通过 `emcmotStatus.id`、`depth`、`activeDepth`、`paused`、`current_vel` 等回传。
|
||||
|
||||
### 8.4 状态记录方式
|
||||
|
||||
Run 后主要状态变化:
|
||||
|
||||
| 字段 | 变化 |
|
||||
| --- | --- |
|
||||
| `task.mode` | `AUTO` |
|
||||
| `task.interpState` | `READING` |
|
||||
| `task.task_paused` | `0` |
|
||||
| `motion.traj.single_stepping` | `0` |
|
||||
| `motion.traj.queue/activeQueue` | 随 motion 队列变化 |
|
||||
| `motion.traj.id/tag` | 跟随当前运动段与源代码 state tag |
|
||||
|
||||
`emcStatus->task.status` 顶层状态由 `emctaskmain.cc:3458-3476` 合成:有错误为 `ERROR`,全部完成且解释器 idle 为 `DONE`,否则为 `EXEC`。
|
||||
|
||||
## 9. 暂停 Pause 按钮
|
||||
|
||||
### 9.1 AXIS UI 调用链
|
||||
|
||||
源码:`linuxcnc/bin/axis:2402-2406`
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
AXIS Pause 的严格条件:
|
||||
|
||||
- `task_mode == MODE_AUTO`
|
||||
- `interp_state in (INTERP_READING, INTERP_WAITING)`
|
||||
- 不是仅凭 `runState == running`
|
||||
|
||||
工具栏合并暂停/恢复 `task_pauseresume()` 源码:`linuxcnc/bin/axis:2433-2443`
|
||||
|
||||
- 仅 `MODE_AUTO` 或 `MODE_MDI` 可操作。
|
||||
- 如果 `s.paused` 为真,发送 resume。
|
||||
- 否则如果解释器不是 idle,发送 pause。
|
||||
|
||||
### 9.2 task/motion 调用链
|
||||
|
||||
```text
|
||||
c.auto(AUTO_PAUSE)
|
||||
-> EMC_TASK_PLAN_PAUSE
|
||||
-> emcTaskPlan()
|
||||
-> emcTaskIssueCommand()
|
||||
-> emcTrajPause()
|
||||
-> EMCMOT_PAUSE
|
||||
-> tpPause()
|
||||
```
|
||||
|
||||
task 处理源码:`linuxcnc/src/emc/task/emctaskmain.cc:2337-2345`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `emcTrajPause()`
|
||||
- 若当前不是 `PAUSED`,保存 `interpResumeState = interpState`
|
||||
- `task.interpState = PAUSED`
|
||||
- `task.task_paused = 1`
|
||||
|
||||
`emcTrajPause()` 源码:`linuxcnc/src/emc/task/taskintf.cc:1417-1421`
|
||||
|
||||
motion 侧 `EMCMOT_PAUSE` 源码:`linuxcnc/src/emc/motion/command.c:1234-1240`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `tpPause(&coord_tp)`
|
||||
- `emcmotStatus->paused = 1`
|
||||
|
||||
### 9.3 状态记录方式
|
||||
|
||||
Pause 后必须同时体现:
|
||||
|
||||
- `task.interpState = PAUSED`
|
||||
- `task.task_paused = 1`
|
||||
- `motion.traj.paused = 1`
|
||||
- `interpResumeState` 保存暂停前的 `READING` 或 `WAITING`
|
||||
|
||||
Web 项目必须避免只改 UI `runState = paused`,否则 resume/step 会缺少 LinuxCNC 必需的恢复上下文。
|
||||
|
||||
## 10. 单步 Step 按钮
|
||||
|
||||
### 10.1 AXIS UI 调用链
|
||||
|
||||
源码:`linuxcnc/bin/axis:2395-2400`
|
||||
|
||||
```python
|
||||
def task_step(*event):
|
||||
if s.task_mode != linuxcnc.MODE_AUTO or s.interp_state != linuxcnc.INTERP_IDLE:
|
||||
o.set_highlight_line(None)
|
||||
if run_warn(): return
|
||||
ensure_mode(linuxcnc.MODE_AUTO)
|
||||
c.auto(linuxcnc.AUTO_STEP)
|
||||
```
|
||||
|
||||
AXIS Step 的含义不是“前端播放下一帧”,而是向 task 发送 `EMC_TASK_PLAN_STEP`。task 根据解释器状态决定:
|
||||
|
||||
- AUTO/IDLE:先启动程序,再立刻暂停,进入单步准备态。
|
||||
- AUTO/READING 或 WAITING:设置 single stepping,下一段读入/执行后停住。
|
||||
- AUTO/PAUSED:若 motion 队列中有暂停运动,发 `EMCMOT_STEP` 让 motion 走到下一段;否则恢复解释器状态继续读。
|
||||
|
||||
### 10.2 task 处理
|
||||
|
||||
AUTO/IDLE 中 `STEP` 源码:`linuxcnc/src/emc/task/emctaskmain.cc:1078-1091`
|
||||
|
||||
执行内容:
|
||||
|
||||
- 构造 `taskPlanRunCmd.line = 0`
|
||||
- 调 `emcTaskIssueCommand(&taskPlanRunCmd)` 启动程序
|
||||
- `emcTrajPause()`
|
||||
- 保存 `interpResumeState`
|
||||
- `task.interpState = PAUSED`
|
||||
- `task.task_paused = 1`
|
||||
|
||||
AUTO/READING 中 `STEP` 源码:`linuxcnc/src/emc/task/emctaskmain.cc:1178-1182`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `motion.traj.single_stepping = 1`
|
||||
- `stepping = 1`
|
||||
- `steppingWait = 0`
|
||||
|
||||
AUTO/PAUSED 中 `STEP` 源码:`linuxcnc/src/emc/task/emctaskmain.cc:1254-1266`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `motion.traj.single_stepping = 1`
|
||||
- `stepping = 1`
|
||||
- 如果 `motion.traj.paused && queue > 0`,调用 `emcTrajStep()`
|
||||
- 否则 `task.interpState = interpResumeState`
|
||||
- `task.task_paused = 1`
|
||||
|
||||
AUTO/WAITING 中 `STEP` 源码:`linuxcnc/src/emc/task/emctaskmain.cc:1330-1334`
|
||||
|
||||
执行内容同 READING:设置单步标志。
|
||||
|
||||
### 10.3 motion 处理
|
||||
|
||||
`emcTrajStep()` 源码:`linuxcnc/src/emc/task/taskintf.cc:1438-1442`
|
||||
|
||||
motion 侧 `EMCMOT_STEP` 源码:`linuxcnc/src/emc/motion/command.c:1265-1273`
|
||||
|
||||
处理逻辑:
|
||||
|
||||
- 如果 motion 当前 paused:
|
||||
- 记录 `idForStep = emcmotStatus->id`
|
||||
- `emcmotStatus->stepping = 1`
|
||||
- resume 轨迹规划器,使其执行到 id 变化或下一步边界再停。
|
||||
|
||||
### 10.4 状态记录方式
|
||||
|
||||
单步相关字段:
|
||||
|
||||
- task 侧静态变量:`stepping`、`steppingWait`
|
||||
- task 可见状态:`emcStatus->motion.traj.single_stepping`
|
||||
- motion 状态:`emcmotStatus->stepping`、`idForStep`、`paused`
|
||||
- 解释器状态:`PAUSED` 或恢复到 `interpResumeState`
|
||||
|
||||
Web 中若只实现“数组 frameIndex++”,不能等价于 LinuxCNC Step。正确做法是把 Step 作为 task 命令,驱动 task/motion 的 pause/step 状态机。
|
||||
|
||||
## 11. Resume 补充
|
||||
|
||||
虽然用户重点列出“急停、上电、Home、执行、暂停、单步执行”,但暂停机制必须包含 Resume 才完整。
|
||||
|
||||
AXIS `task_resume()` 源码:`linuxcnc/bin/axis:2424-2431`
|
||||
|
||||
条件:
|
||||
|
||||
- `s.paused` 为真。
|
||||
- `task_mode` 是 `AUTO` 或 `MDI`。
|
||||
- 发送 `AUTO_RESUME`。
|
||||
|
||||
task 处理源码:`linuxcnc/src/emc/task/emctaskmain.cc:2369-2376`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `emcTrajResume()`
|
||||
- `task.interpState = interpResumeState`
|
||||
- `task.task_paused = 0`
|
||||
- `motion.traj.single_stepping = 0`
|
||||
- 清 `stepping`、`steppingWait`
|
||||
|
||||
motion 侧 `EMCMOT_RESUME` 源码:`linuxcnc/src/emc/motion/command.c:1256-1263`
|
||||
|
||||
执行内容:
|
||||
|
||||
- `emcmotStatus->stepping = 0`
|
||||
- `tpResume()`
|
||||
- `emcmotStatus->paused = 0`
|
||||
|
||||
## 12. 按钮前置条件汇总
|
||||
|
||||
| 按钮 | AXIS UI 前置条件 | task/motion 硬条件 | 执行后状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 急停 | 无;读 `s.task_state` 决定 reset 或 estop | `EMC_TASK_SET_STATE` 在多数状态允许 | `ESTOP` 分支 abort、disable、IO estop on;reset 分支 IO estop off、abort、同步解释器 |
|
||||
| 上电 | 只有 `STATE_ESTOP_RESET` 发送 ON,否则发送 OFF | ON 要 motion enable 输入为真;OFF 总是 abort/disable | ON 后由 `motion.traj.enabled` 推导 `STATE_ON`;OFF 后推导 `ESTOP_RESET` 或 OFF 语义并清 volatile home |
|
||||
| Home | `manual_ok()`;切 `MODE_MANUAL`;必要时 re-home 确认 | motion 必须 FREE、enabled、未 homing、未 inhibit | `joint.N.homing` 过程态;完成 `joint.N.homed=1`;失败清 homed |
|
||||
| Run | `run_warn()`;切 `MODE_AUTO` | ON/AUTO/IDLE;已 homed 或 no_force_homing;程序打开 | `interpState=READING`,`task_paused=0`,motion 队列开始增长 |
|
||||
| Pause | `MODE_AUTO` 且 `INTERP_READING/WAITING` | `EMC_TASK_PLAN_PAUSE`;motion `EMCMOT_PAUSE` | `interpState=PAUSED`,`task_paused=1`,`motion.paused=1`,保存 resume state |
|
||||
| Step | 切 `MODE_AUTO`;非 idle 时可先 run_warn | 按 AUTO/IDLE/READING/PAUSED/WAITING 分支处理 | 设置 `single_stepping`;必要时 `EMCMOT_STEP`;保持/进入 paused |
|
||||
| Resume | `paused` 且 `MODE_AUTO/MDI` | `EMC_TASK_PLAN_RESUME`;motion `EMCMOT_RESUME` | 恢复 `interpResumeState`,清 `task_paused/single_stepping/stepping` |
|
||||
|
||||
## 13. WASM-port 完善建议
|
||||
|
||||
当前 `wasm-port` 已有 task/HAL WASM 入口:
|
||||
|
||||
- `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp`
|
||||
- `wasm-port/runtime/sdk/src/linuxcnc-task-hal.js`
|
||||
|
||||
但是 `linuxcnc_task_hal_wasm.cpp` 仍是 `linuxcnc_task_motion_hal_wasm_phase4_minimal`,存在明显过渡实现:
|
||||
|
||||
- 用字符串搜索和手写 JSON 解析命令。
|
||||
- 自己维护 `TaskRuntime.state/mode/interp_state/task_paused/single_stepping`。
|
||||
- 从 G-code 文本中用简单规则派生 motion,存在 G-code 语义重实现风险。
|
||||
- `readiness()` 中仍标记 `nativeTaskReady:false`、`nativeHalSyncReady:false`。
|
||||
|
||||
这与 `wasm-port/AGENTS.md` 的要求冲突风险较高:项目应复用 LinuxCNC source,不应扩展项目自写 CNC 语义。
|
||||
|
||||
建议按以下阶段完善。
|
||||
|
||||
### 13.1 第一阶段:把状态枚举和门禁表固化为 LinuxCNC 镜像
|
||||
|
||||
目标:先不改大架构,但移除随意字符串状态。
|
||||
|
||||
建议:
|
||||
|
||||
1. 在 `runtime/core/linuxcnc_wrap` 增加 task 状态模型头文件,例如 `linuxcnc_task_state_model.hh`。
|
||||
2. 用与 LinuxCNC 一致的枚举名:`ESTOP`、`ESTOP_RESET`、`ON`、`MANUAL`、`AUTO`、`MDI`、`IDLE`、`READING`、`PAUSED`、`WAITING`。
|
||||
3. 实现一个 `task_accepts_command(state, mode, interp, command)`,逐项映射 `emcTaskPlan()` 的允许表。
|
||||
4. 所有 `lctask_send_command_json()` 先走该门禁表,返回明确错误码和错误文本。
|
||||
5. 增加测试覆盖:
|
||||
- ESTOP 下仅允许 state/mode/config 等 immediate 命令。
|
||||
- ON/MANUAL 下允许 HOME,不允许 AUTO RUN。
|
||||
- ON/AUTO/IDLE 下允许 RUN/STEP。
|
||||
- ON/AUTO/READING 下允许 PAUSE/STEP,不允许 HOME。
|
||||
- ON/AUTO/PAUSED 下允许 RESUME/STEP。
|
||||
|
||||
### 13.2 第二阶段:替换手写 G-code motion 派生
|
||||
|
||||
当前 `enqueue_linear_move_from_line()` 和 Web 侧 `buildSourceLineMotionSegments()` 都会从 G-code 行解析轴和 feed,这属于语义漂移高风险点。
|
||||
|
||||
正确方向:
|
||||
|
||||
1. G-code 到 canonical motion 必须由 vendored LinuxCNC interpreter 产生。
|
||||
2. task/HAL runtime 只消费 LinuxCNC canonical 输出或已验证的 motion plan,不自行解释 G-code。
|
||||
3. 对没有 motion plan 的程序,应返回“需要 interpreter plan”错误,而不是 fallback 解析 `G0/G1`。
|
||||
4. `buildTaskHalProgramMotionPlan()` 可保留为 host 边界适配器,但输入必须来自 LinuxCNC interpreter runtime 的 canonical events,而不是源码行正则解析。
|
||||
|
||||
### 13.3 第三阶段:引入 vendored task/motion 源码闭环
|
||||
|
||||
最终目标不是仿照 `emctaskmain.cc` 写一份 JS/C++ 状态机,而是逐步把 vendored LinuxCNC task/motion 代码纳入 WASM:
|
||||
|
||||
1. 将 `emc/nml_intf/emc_nml.hh` 中的命令/状态结构作为 WASM 内部 ABI 参考。
|
||||
2. 用内存队列替代 NML transport,但保留 `EMC_TASK_*` / `EMCMOT_*` 命令类型。
|
||||
3. 将 `taskintf.cc` 的 `emcTrajEnable/Pause/Resume/Step/Home` 路由接到 `linuxcnc_motion_runtime.c`。
|
||||
4. 将 `emcTaskSetState()`、`emcTaskSetMode()`、`emcTaskAbort()` 的语义直接迁移或编译进 WASM。
|
||||
5. motion runtime 应逐步引入 `command.c`、`homing.c`、`control.c` 中真实状态更新,而不是只暴露“看起来相同”的 JSON status。
|
||||
|
||||
### 13.4 第四阶段:状态发布完全对齐 `emcStatus`
|
||||
|
||||
WASM SDK `readStatus()` 应输出与 Python `linuxcnc.stat()` 结构同构的字段:
|
||||
|
||||
- `task.state`
|
||||
- `task.mode`
|
||||
- `task.interpState`
|
||||
- `task.execState`
|
||||
- `task.taskPaused`
|
||||
- `motion.traj.enabled`
|
||||
- `motion.traj.paused`
|
||||
- `motion.traj.singleStepping`
|
||||
- `motion.traj.queue`
|
||||
- `motion.traj.id`
|
||||
- `motion.joint[n].homed`
|
||||
- `motion.joint[n].homing`
|
||||
- `motion.joint[n].homeState`
|
||||
- `echoSerialNumber`
|
||||
- `commandType`
|
||||
|
||||
这样 Web 项目可以直接消费 LinuxCNC 状态,而不是维护第二套状态。
|
||||
|
||||
## 14. web-rtcp-5axis-xyzbc-trt-sim-plan 完善建议
|
||||
|
||||
当前 Web 项目已有:
|
||||
|
||||
- `app/src/state/linuxcnc-task-policy.js`
|
||||
- `app/src/state/store.js`
|
||||
- `app/src/runtime/linuxcnc-task-hal-runtime.js`
|
||||
- browser/node 对按钮路径的验证
|
||||
|
||||
这已经完成一部分 AXIS 对标,但还需要继续收敛。
|
||||
|
||||
### 14.1 状态源优先级必须调整
|
||||
|
||||
建议状态优先级:
|
||||
|
||||
```text
|
||||
taskHalStatus.ui/task/motion 原生状态
|
||||
> LinuxCNC interpreter canonical events
|
||||
> Web UI 派生状态
|
||||
```
|
||||
|
||||
Web store 中 `machine.taskState/mode/interpState/runState` 应逐渐变成 task/HAL 状态的投影,而不是独立真相源。
|
||||
|
||||
### 14.2 按钮 action 应发送 LinuxCNC 命令,不直接改业务状态
|
||||
|
||||
按钮处理建议统一为:
|
||||
|
||||
```text
|
||||
UI click
|
||||
-> gateLinuxCncTaskAction()
|
||||
-> taskHalRuntime.sendCommand({ type: "EMC_TASK_*" })
|
||||
-> taskHalRuntime.runCycles()
|
||||
-> taskHalRuntime.readStatus()
|
||||
-> store 接收 STATUS_SYNC
|
||||
-> UI 渲染
|
||||
```
|
||||
|
||||
避免:
|
||||
|
||||
- click 后直接 `runState = "running"`;
|
||||
- click 后直接 `machine.allHomed = true`;
|
||||
- pause/step 直接操作播放帧;
|
||||
- power 直接改 `powerOn` 而不等 motion enabled/status。
|
||||
|
||||
### 14.3 补齐 Home 状态过程态
|
||||
|
||||
Web 当前常见实现是 `HOME` 后立即 `allHomed=true`。LinuxCNC 真实机制是:
|
||||
|
||||
```text
|
||||
EMC_JOINT_HOME
|
||||
-> joint.N.homing = true
|
||||
-> home_state 多阶段变化
|
||||
-> joint.N.homed = true
|
||||
```
|
||||
|
||||
建议 Web 状态增加:
|
||||
|
||||
- `machine.joints[n].homing`
|
||||
- `machine.joints[n].homed`
|
||||
- `machine.joints[n].homeState`
|
||||
- `machine.homingActive`
|
||||
- `machine.allHomed`
|
||||
|
||||
按钮门禁:
|
||||
|
||||
- homing 过程中禁止再次 Home。
|
||||
- Run 前使用 `allHomed || noForceHoming`。
|
||||
- OFF/ESTOP 时清 volatile home,而不是无条件清所有 home;如果当前模型无法区分 volatile,先在文档和状态字段中标注“volatile home 简化”。
|
||||
|
||||
### 14.4 补齐 Step 的 LinuxCNC 语义
|
||||
|
||||
Web 需要区分三种 Step:
|
||||
|
||||
1. 程序 idle 首次 step:等价于 RUN 后立即 PAUSE。
|
||||
2. 程序 running 中 step:设置 single stepping,等待下一段边界停住。
|
||||
3. 程序 paused 中 step:若 motion queue 有内容,发送 motion step;否则恢复 interpreter 读下一段。
|
||||
|
||||
建议在 `linuxcnc-task-policy.js` 中把 `STEP` 门禁拆开:
|
||||
|
||||
- `canStepFromIdle`
|
||||
- `canStepWhileReading`
|
||||
- `canStepWhilePaused`
|
||||
- `canStepWhileWaiting`
|
||||
|
||||
并在 status 中显示:
|
||||
|
||||
- `task.singleStepping`
|
||||
- `motion.stepping`
|
||||
- `motion.idForStep`
|
||||
- `motion.paused`
|
||||
|
||||
### 14.5 Pause/Resume 继续保持 AXIS 门禁
|
||||
|
||||
已完成的严格对标应继续保留:
|
||||
|
||||
- `PAUSE`:仅 `STATE_ON + MODE_AUTO + INTERP_READING/WAITING`。
|
||||
- `PAUSE_RESUME`:仅 `MODE_AUTO/MDI`;paused 时 resume;非 idle 时 pause。
|
||||
- `RESUME`:必须 `paused && MODE_AUTO/MDI`。
|
||||
|
||||
后续测试不要只断言按钮可点击,应断言 task/HAL status:
|
||||
|
||||
- pause 后 `task.interpState=PAUSED`
|
||||
- pause 后 `task.taskPaused=true`
|
||||
- pause 后 `motion.paused=true`
|
||||
- resume 后恢复 `interpResumeState`
|
||||
- step 后 `singleStepping=true`
|
||||
|
||||
### 14.6 5 轴 XYZBC-TRT 特有要求
|
||||
|
||||
5 轴仿真不应把主按钮逻辑和 RTCP/kinematics 逻辑混在一起。建议分层:
|
||||
|
||||
1. task/motion 状态机:只负责开关机、home、run、pause、step。
|
||||
2. kinematics/RTCP:只负责 XYZBC-TRT 位置变换和 TCP 显示。
|
||||
3. interpreter/remap:负责 M428/M429/M430、switchkins、G-code 语义。
|
||||
4. UI:只渲染状态并发送命令。
|
||||
|
||||
Run 前置条件除 LinuxCNC 通用条件外,还应检查:
|
||||
|
||||
- 选定 profile 的 INI 已加载;
|
||||
- machine files 已 staged;
|
||||
- 当前 G-code 已 open 到 task/HAL session;
|
||||
- kinematics runtime 与 task/HAL runtime readiness 均为 true;
|
||||
- 如果程序依赖 switchkins/remap,HAL `motion.switchkins-type` 同步状态应可观测。
|
||||
|
||||
## 15. 推荐验证矩阵
|
||||
|
||||
### 15.1 LinuxCNC 行为矩阵
|
||||
|
||||
用本地 LinuxCNC 源码为基准建立 golden matrix:
|
||||
|
||||
| 初始状态 | 命令 | 期望 |
|
||||
| --- | --- | --- |
|
||||
| ESTOP/MANUAL/IDLE | ESTOP button | `STATE_ESTOP_RESET` |
|
||||
| ESTOP_RESET/MANUAL/IDLE | Power | `STATE_ON` |
|
||||
| ESTOP/MANUAL/IDLE | Power | 不上电,走 OFF 分支 |
|
||||
| ON/MANUAL/IDLE | Home All | `joint.*.homing -> homed` |
|
||||
| ON/AUTO/IDLE + unhomed | Run | 拒绝,提示未 homed |
|
||||
| ON/AUTO/IDLE + homed | Run | `interpState=READING` |
|
||||
| ON/AUTO/READING | Pause | `interpState=PAUSED`、`motion.paused=true` |
|
||||
| ON/AUTO/PAUSED | Resume | 恢复 reading/waiting |
|
||||
| ON/AUTO/IDLE + homed | Step | run + pause,single step 准备 |
|
||||
| ON/AUTO/PAUSED + queue>0 | Step | `EMCMOT_STEP`,执行下一段后停 |
|
||||
|
||||
### 15.2 wasm-port 测试建议
|
||||
|
||||
新增或扩展:
|
||||
|
||||
- `tests/wasm/node/verify_task_state_matrix.mjs`
|
||||
- `tests/wasm/node/verify_task_home_state_machine.mjs`
|
||||
- `tests/wasm/node/verify_task_pause_resume_step.mjs`
|
||||
|
||||
每个测试不要只看返回码,要读 `lctask_read_status_json()`:
|
||||
|
||||
- task 字段;
|
||||
- motion 字段;
|
||||
- HAL pins;
|
||||
- events;
|
||||
- command echo。
|
||||
|
||||
### 15.3 Web 测试建议
|
||||
|
||||
在现有 `verify_xyzbc_trt_web_app.mjs` 和 browser smoke 上补齐:
|
||||
|
||||
- ESTOP/RESET/POWER 不允许绕过;
|
||||
- Home 有过程态;
|
||||
- Run 未 homed 拒绝;
|
||||
- Pause idle 拒绝;
|
||||
- Pause manual 拒绝;
|
||||
- Step idle 首次启动后暂停;
|
||||
- Step paused 推进一段;
|
||||
- Resume 清 single stepping;
|
||||
- Store 状态与 taskHalStatus 一致。
|
||||
|
||||
## 16. 实施优先级
|
||||
|
||||
建议按以下顺序推进:
|
||||
|
||||
1. `wasm-port` 先消除 task/HAL runtime 中的 G-code 正则解析 fallback,要求 motion plan 必须来自 LinuxCNC interpreter canonical output。
|
||||
2. `wasm-port` 把 `lctask_send_command_json()` 改为显式命令结构解析和 LinuxCNC 状态门禁表。
|
||||
3. `wasm-port` 补齐 `task/motion/joint` 状态字段,使 `readStatus()` 接近 `linuxcnc.stat()`。
|
||||
4. Web store 改为 taskHalStatus 投影源,减少直接改 `runState/machine` 的分支。
|
||||
5. Web Home/Step 加过程态和真实 task/motion 状态断言。
|
||||
6. 再继续推进 vendored `emctask.cc/taskintf.cc/command.c/homing.c` 的更直接编译或等价适配。
|
||||
|
||||
最终目标:Web 按钮只是 AXIS 的浏览器外壳,状态机和 CNC 语义来自 LinuxCNC 源码或其受控 WASM 适配层。
|
||||
723
项目分析/AXIS主界面按钮调用链与WASM完善建议.md
Normal file
723
项目分析/AXIS主界面按钮调用链与WASM完善建议.md
Normal file
@@ -0,0 +1,723 @@
|
||||
# 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 的乐观赋值。
|
||||
|
||||
Reference in New Issue
Block a user