26 KiB
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 按钮的完整链路是:
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 定义:
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_states.task_modes.interp_states.pauseds.task_pauseds.homed[]s.motion_modes.file
2.2 task_state 的真实来源
emcTaskSetState() 执行的是动作,最终显示的 task.state 由 determineState() 根据下级状态推导:
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 轨迹模式推导:
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 主循环每周期执行:
emcCommandBuffer->read()
emcTaskPlan()
emcTaskExecute()
emcMotionUpdate(&emcStatus->motion)
emcTaskUpdate(&emcStatus->task)
emcStatusBuffer->write(emcStatus)
AXIS 的 AxisCanon.update() 通过 self.stat.poll() 读取 status,再写入 Tk 变量:
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 |
| 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 调用链
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) 主要做:
emcMotionAbort()
emcSpindleAbort()
emcAuxEstopOn()
emcTrajDisable()
emcCoolantFloodOff()
emcTaskAbort()
emcIoAbort(TASK_STATE_ESTOP)
emcJointUnhome(-2) # 仅 volatile_home 关节
emcAbortCleanup()
emcTaskPlanSynch()
随后 determineState() 因 io.aux.estop == true 返回 ESTOP。emcTaskAbort() 同时清理:
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):
emcAuxEstopOff()
emcCoolantFloodOff()
emcTaskAbort()
emcIoAbort(TASK_STATE_ESTOP_RESET)
emcSpindleAbort()
emcAbortCleanup()
emcTaskPlanSynch()
此时 motion 仍未 enable,determineState() 返回 ESTOP_RESET,AXIS 状态显示为 OFF。
5. 上电/下电
5.1 调用链
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):
emcTrajEnable()
emcCoolantFloodOff()
motion 收到 EMCMOT_ENABLE 后置轨迹/关节使能。下一轮 determineState() 看到:
io.aux.estop == false
motion.traj.enabled == true
于是 task.state = ON。AXIS 轮询后更新按钮:Run/Home/MDI/Jog 类动作开始具备进一步门禁条件。
5.4 下电后的状态记录
emcTaskSetState(OFF):
emcMotionAbort()
emcSpindleAbort()
emcTrajDisable()
emcIoAbort(TASK_STATE_OFF)
emcCoolantFloodOff()
emcTaskAbort()
emcJointUnhome(-2)
emcAbortCleanup()
emcTaskPlanSynch()
下一轮 determineState() 通常显示 ESTOP_RESET,同时解释器和单步状态被清空。配置为 volatile home 的关节会失去 homed。
6. Home
6.1 调用链
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_ONinterp_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_FREEmotion.homing-inhibit未置位- 没有其他 homing 正在进行
- motion enable flag 为真
6.3 Home 状态记录
taskintf.cc emcJointHome() 只写 motion 命令:
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时 jointhomed=1、homing=0、home state 回到HOME_IDLE。base_get_allhomed()遍历所有 active joints,只要一个未 homed 就返回 false。
task 状态回填路径:
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 调用链
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_ONinterp_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 入口设置:
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 调用链
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_ONtask_mode == MODE_AUTOinterp_state in (READING, WAITING)
工具栏 Pause/Resume:
task_state == STATE_ONtask_mode in (AUTO, MDI)interp_state != IDLE
8.3 Pause 状态记录
task 收到 PLAN_PAUSE:
emcTrajPause()
if task.interpState != PAUSED:
interpResumeState = task.interpState
task.interpState = PAUSED
task.task_paused = 1
motion 收到 EMCMOT_PAUSE:
tpPause(&coord_tp)
emcmotStatus->paused = 1
AXIS 同时看:
interp_state == PAUSEDs.paused/interp_pause != 0task_paused
因此 Pause 工具栏按钮 relief 变成 sunken。
8.4 Resume 调用链与状态
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 调用链
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_ONtaskfile != ""
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 全局变量:
stepping = 1
steppingWait = 0
motion.traj.single_stepping = 1
STEPPING_CHECK() 是 emcTaskExecute() 中每个等待状态前的宏:
- 第一次进入时记录
steppedLine = currentLine并置steppingWait = 1。 - 后续若
currentLine已改变,break,等待下一次 step。
motion 层 EMCMOT_STEP:
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 每周期检测:
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 语义源。
建议按以下顺序推进:
-
建立 LinuxCNC task 状态结构镜像
在 WASM runtime 中用枚举替代字符串:EMC_TASK_STATE、EMC_TASK_MODE、EMC_TASK_INTERP、EMC_TASK_EXEC。保留对外 JSON 小写/字符串只是 SDK 边界转换。 -
复刻
determineState()与determineMode()
不要让SET_STATE ON直接等于task.state=ON。应维护:io.aux.estopmotion.traj.enabledmotion.traj.modemdiOrAuto然后每个 task cycle 推导task.state/task.mode。
-
把
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 清除
-
移植或包装
homing.c
Home 不能长期用“直接置 allHomed”替代。应把 vendoredmotion/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。
-
补齐 Step 双层语义
task 层实现stepping/steppingWait/steppedLine,motion 层保留idForStep,当 motion id 改变时 pause。当前 runCycles 中“执行一次后 paused”的做法应替换为 LinuxCNC 的 line/id 驱动。 -
用 LinuxCNC status 字段作为唯一真实状态源
SDKreadStatus()应返回 task/motion status;Web store 只做投影,不再自行判定“已经暂停/已经回零/已经上电”。 -
增加 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 更薄:
-
保留
linuxcnc-task-policy.js作为 UI 门禁投影
它可以继续计算按钮 disabled 状态,但输入应优先来自taskHalRuntime.readStatus(),而不是 store 自己推导。 -
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 中提前“宣布成功”。
- 急停:发
-
修正 Power 的 UI/回调双语义
AXIS UI 禁用 ESTOP 下电源按钮;但onoff_clicked()若被直接调用且非ESTOP_RESET会发 OFF。Web 应:- UI disabled 对标
task_state == estop。 - 内部命令测试保留“直接调用 TOGGLE_POWER 时非 estop-reset -> OFF”的回调语义。
- UI disabled 对标
-
Home 状态不要直接
allHomed=true
先作为过渡兼容可以保留,但应标记为 fallback。正式路径要从 WASM status 的 per-jointhomed[]汇总。 -
Pause/Resume 需要同时显示 task 与 motion 暂停
LinuxCNC 中暂停状态由task.interpState=PAUSED、task.task_paused=1、motion.paused=1共同组成。Web UI 里的runState="paused"只能作为显示投影,不应是源状态。 -
Step 测试要覆盖三种状态
- AUTO + IDLE:首次 Step 等价 run from start 后暂停。
- AUTO + READING/WAITING:设置 task stepping,下一条/下一段后暂停。
- AUTO + PAUSED 且 motion queue > 0:发
EMCMOT_STEP,motion id 改变后暂停。
-
证据文件增加字段级 diff
当前 evidence 可继续保留截图和 pass/fail;建议新增 JSON diff:task.state/mode/interpState/execState/taskPausedmotion.enabled/paused/stepping/idjoint[].homed/homing/enabled/inposcurrentLine/readLine/motionLine
13. 推荐验收用按钮序列
用于 native LinuxCNC、wasm-port、web-rtcp 三方对齐:
初始: 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 的乐观赋值。