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

26 KiB
Raw Permalink Blame History

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 按钮的完整链路是:

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_STATEEMC_TASK_MODEEMC_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_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.statedetermineState() 根据下级状态推导:

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
  • 切 MDImotion 进 COORDabort 当前任务并同步解释器,mdiOrAuto = MDI
  • 切 AUTOmotion 进 COORDabort 当前任务并同步解释器,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 返回 ESTOPemcTaskAbort() 同时清理:

interpState = IDLE
execState = DONE
task_paused = 0
motionLine/readLine/current command 清零
stepping = 0
steppingWait = 0
interp_list.clear()

4.4 解除急停

同一个按钮在当前 STATE_ESTOP 时发 STATE_ESTOP_RESETemcTaskSetState(ESTOP_RESET)

emcAuxEstopOff()
emcCoolantFloodOff()
emcTaskAbort()
emcIoAbort(TASK_STATE_ESTOP_RESET)
emcSpindleAbort()
emcAbortCleanup()
emcTaskPlanSynch()

此时 motion 仍未 enabledetermineState() 返回 ESTOP_RESETAXIS 状态显示为 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_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 命令:

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=1homing=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_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 入口设置:

motion.traj.single_stepping = 0
stepping = 0
steppingWait = 0
programStartLine = run_msg->line
task.interpState = READING
task.task_paused = 0

执行中:

  • emcTaskPlan() 读取解释器行,设置 readLinecommand
  • 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 回到 IDLEexecState 回到 DONEAXIS 的 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_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

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 == PAUSED
  • s.paused / interp_pause != 0
  • task_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_ON
  • taskfile != ""

Python/task

  • 若当前不是 AUTO+IDLEtask_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=1task.state=ON
下电 电源按钮在非 ESTOP_RESET 分支 无需 IDLE属于立即命令 EMC_TASK_SET_STATE OFF abort、motion disabled、volatile home 清除、通常显示 ESTOP_RESET
Home STATE_ON && INTERP_IDLEPython 还要求 manual_ok() MANUAL/FREE、motion enabled、无 homing inhibit、无其他 homing EMC_JOINT_HOME -> EMCMOT_JOINT_HOME joint.N.homing 过程变化,完成后 joint.N.homed=1all_homed() 变真
Run STATE_ON && INTERP_IDLE && taskfile != "" AUTO、已回零或 no_force_homing、程序已打开 EMC_TASK_PLAN_RUN interpState=READINGtask_paused=0、解释器生成 motion 队列
Pause 菜单要求 STATE_ON && READING/WAITING;工具栏要求非 IDLE AUTO或工具栏允许 AUTO/MDI EMC_TASK_PLAN_PAUSE -> EMCMOT_PAUSE interpResumeState=原状态interpState=PAUSEDtask_paused=1motion.paused=1
Resume STATE_ON && INTERP_PAUSED 或工具栏 paused AUTO/MDI、s.paused、无 resume inhibit EMC_TASK_PLAN_RESUME -> EMCMOT_RESUME interpState=interpResumeStatetask_paused=0motion.paused=0、单步清零
Step STATE_ON && taskfile != "" AUTO不同 interpState 分支处理 EMC_TASK_PLAN_STEP,必要时 EMCMOT_STEP single_stepping=1stepping=1、执行一个 task/motion id 后回 PAUSED

11. 对 wasm-port 的完善建议

wasm-port 当前方向正确:它已经有 runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpplinuxcnc_motion_runtime.c、SDK runtime/sdk/src/linuxcnc-task-hal.js,并通过 lctask_send_command_json() 接收类似 EMC_TASK_SET_STATEEMC_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_inhibitHOME_* 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_STATEEMC_TASK_MODEEMC_TASK_INTERPEMC_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/steppedLinemotion 层保留 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.jsstore.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=PAUSEDtask.task_paused=1motion.paused=1 共同组成。Web UI 里的 runState="paused" 只能作为显示投影,不应是源状态。

  6. Step 测试要覆盖三种状态

    • AUTO + IDLE首次 Step 等价 run from start 后暂停。
    • AUTO + READING/WAITING设置 task stepping下一条/下一段后暂停。
    • AUTO + PAUSED 且 motion queue > 0EMCMOT_STEPmotion 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-portweb-rtcp 三方对齐:

初始: 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 的乐观赋值。