docs: record pause wasm implementation plan

This commit is contained in:
wangdequan
2026-07-07 09:40:33 -04:00
parent 6cecd280e2
commit 83402c506e
3299 changed files with 1364681 additions and 325 deletions

View File

@@ -1,5 +1,112 @@
# GPT/Codex 执行过程日志
## 2026-07-07 09:14 EDT
### 提问
用户要求分析 `/home/mes123456/cnc_wams/linuxcnc` 源程序中 AXIS 主界面“暂停按钮”的详细调用链条,包括 task、motion 的 C/C++ 如何实现暂停按钮功能;因为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的暂停按钮一直不好使用,要求将分析结果写入单独文档。
### 执行过程
1. 使用 `rg``linuxcnc/src``linuxcnc/share` 中搜索 `task_pauseresume``task_pause``task_resume``EMC_TASK_PLAN_PAUSE``EMCMOT_PAUSE``tpPause` 等关键字,定位到 AXIS UI、Python 扩展、task、motion、TP 轨迹规划器相关源码。
2. 查看 `git status --short`,确认仓库已有大量未提交改动和未跟踪文件;本次只新增分析文档并追加日志,不回退既有内容。
3. 读取 `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py` 的关键区域,确认 `task_pause()``MODE_AUTO` 且 interpreter 为 `READING/WAITING` 时发送 `c.auto(linuxcnc.AUTO_PAUSE)``task_resume()``s.paused` 且模式为 AUTO/MDI 时发送 `AUTO_RESUME``task_pauseresume()` 是工具栏 toggle 行为,已暂停则 resume否则 interpreter 非 idle 时 pause。
4. 读取 `linuxcnc/share/axis/tcl/axis.tcl`,确认菜单 Pause/Resume 分别绑定 `task_pause``task_resume`,工具栏 `.toolbar.program_pause` 绑定 `task_pauseresume`
5. 读取 `linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc`,确认 `linuxcnc.command().auto()``LOCAL_AUTO_PAUSE/RESUME/STEP` 分别转换为 `EMC_TASK_PLAN_PAUSE``EMC_TASK_PLAN_RESUME``EMC_TASK_PLAN_STEP` NML 命令并发送。
6. 读取 `linuxcnc/src/emc/nml_intf/emc.hh``emc_nml.hh``emc.cc`,确认 `EMC_TASK_PLAN_PAUSE_TYPE=510``EMC_TASK_PLAN_STEP_TYPE=511``EMC_TASK_PLAN_RESUME_TYPE=512`,以及 `EMC_TASK_INTERP``IDLE/READING/PAUSED/WAITING` 状态定义。
7. 读取 `linuxcnc/src/emc/task/emctaskmain.cc`,确认 task 主循环由 `emcTaskPlan()``emcTaskExecute()` 周期驱动AUTO 模式通过解释器和 `interp_list` 运行immediate command 与 interp list 命令有不同处理路径。
8. 分析 `emctaskmain.cc``EMC_TASK_PLAN_PAUSE_TYPE` 的核心实现:调用 `emcTrajPause()`,保存 `interpResumeState`,将 `task.interpState` 设置为 `PAUSED`,并将 `task.task_paused` 设置为 1。
9. 分析 `emctaskmain.cc``EMC_TASK_PLAN_RESUME_TYPE` 的核心实现:调用 `emcTrajResume()`,将 `task.interpState` 恢复为 `interpResumeState`,清除 `task.task_paused`,并清除 single stepping 状态。
10. 分析 `emctaskmain.cc` 中 PAUSED 状态下执行循环不再从 `interp_list` 取下一条命令的保护逻辑,确认暂停时解释器队列应冻结。
11. 分析 `emctaskmain.cc``EMC_TASK_PLAN_PAUSE_TYPE` 作为 interp list 命令时的 precondition确认排队 pause/optional stop 会等待 motion 和 IO 完成;这与 GUI 即时 pause 不完全相同。
12. 读取 `linuxcnc/src/emc/task/taskintf.cc`,确认 `emcTrajPause()``emcTrajStep()``emcTrajResume()` 分别向 motion 写入 `EMCMOT_PAUSE``EMCMOT_STEP``EMCMOT_RESUME`
13. 读取 `linuxcnc/src/emc/motion/motion.h``motion.c`,确认 motion 命令枚举包含 `EMCMOT_PAUSE/RESUME/STEP``emcmotStatus->paused` 是 motion paused 对外状态,初始化时为 0。
14. 读取 `linuxcnc/src/emc/motion/command.c`,确认 `EMCMOT_PAUSE` 调用 `tpPause(&coord_tp)` 并设置 `emcmotStatus->paused=1``EMCMOT_RESUME` 调用 `tpResume(&coord_tp)` 并设置 `paused=0``EMCMOT_STEP` 在 paused 时记录当前 motion id、短暂 `tpResume()`,并保持 paused 状态。
15. 读取 `linuxcnc/src/emc/motion/control.c`,确认 single step 时 motion id 改变后会自动 `tpPause()`,清 stepping 并保持 `emcmotStatus->paused=1`
16. 读取 `linuxcnc/src/emc/tp/tp_types.h``tp.h``tp.c`,确认 `tpPause()` 只设置 `tp->pausing=1``tpResume()` 设置 `tp->pausing=0`;真正的减速由 TP 规划循环在 pausing 时把 feed scale/velocity control 目标降为 0 实现。
17. 对照读取 Web 项目 `app/src/ui/axis-shell.js`,确认当前工具栏 `tbtn_pause` 绑定 `pause-resume`,方向上对标 AXIS 工具栏 `task_pauseresume`
18. 对照读取 Web 项目 `app/src/state/linuxcnc-task-policy.js`,确认当前 PAUSE/RESUME gating 基本覆盖 task state、mode、interp state但恢复判断主要依赖 `interpState`,缺少对 LinuxCNC `s.paused`/motion paused 的等价建模。
19. 对照读取 Web 项目 `app/src/state/store.js`,确认当前 PAUSE 分支会设置 `interpState="paused"``taskPaused=true``runState="paused"`、速度归零,但文档指出还应确保 sampleIndex、DRO、axisPose、toolhead、runtime TCP 等不继续推进。
20. 新增文档 `web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS暂停按钮LinuxCNC调用链分析.md`,写入 AXIS UI、Python 扩展、NML、task、motion、TP 的完整调用链和 Web 修正建议。
21. 检查新增文档开头和关键字,确认文档包含 `task_pauseresume``EMCMOT_PAUSE``tpPause``emcTrajPause``interpResumeState``motionPaused` 等关键分析点;文档共 694 行。
### 结论
已完成 AXIS 主界面“暂停按钮”的 LinuxCNC 源码调用链分析,并写入独立文档 `web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS暂停按钮LinuxCNC调用链分析.md`。分析结论是 LinuxCNC 暂停由 task/interpreter 暂停和 motion/trajectory 暂停双层模型共同实现task 层设置 `interpState=PAUSED`、保存 `interpResumeState`、设置 `task_paused=1`motion 层通过 `EMCMOT_PAUSE -> tpPause()` 设置 `tp->pausing=1`,由轨迹规划器受控减速到 0 并保留队列。文档同时指出 Web 暂停按钮应补充 `motionPaused` 等价状态、暂停后冻结 sample/pose/DRO/toolhead、严格恢复 `interpResumeState`,并区分菜单 Pause、菜单 Resume 与工具栏 Pause/Resume toggle。
## 2026-07-07 08:59 EDT
### 提问
用户反馈访问 `https://codexcnc.com` 时浏览器出现 Privacy error错误为 `NET::ERR_CERT_AUTHORITY_INVALID`,要求完善网站发布。
### 执行过程
1. 根据截图判断用户访问的是默认 HTTPS 地址 `https://codexcnc.com/`,即 443 端口,而前一次发布主要配置的是 `https://codexcnc.com:8092/`
2. 执行 `curl -vkI https://codexcnc.com/` 检查 443 端口证书,确认当前 443 返回的证书主体为 `CN=82.156.24.101`,并且是自签证书,因此 Chrome 报 `NET::ERR_CERT_AUTHORITY_INVALID`
3. 登录远端服务器,执行 `sudo nginx -T` 检查 Nginx 配置,确认 `codexcnc.com` 仅配置了 `80``8092 ssl`,没有为标准 `443 ssl` 配置专用的 `codexcnc.com` server block。
4. 读取 `/etc/nginx/sites-available/codexcnc-8092`,确认原配置中 `80` 端口跳转到 `https://$host:8092$request_uri``8092` 使用 `/etc/letsencrypt/live/codexcnc.com/` 证书。
5. 在远端备份原配置到 `/var/backups/codexcnc-8092/codexcnc-8092.nginx.20260707205815.before-443.conf`
6. 重写 `/etc/nginx/sites-available/codexcnc-8092`,新增标准 `443 ssl http2` server block`server_name codexcnc.com`,证书使用 `/etc/letsencrypt/live/codexcnc.com/fullchain.pem``/etc/letsencrypt/live/codexcnc.com/privkey.pem`
7.`80` 端口跳转从 `https://$host:8092$request_uri` 调整为标准 `https://$host$request_uri`,使用户访问 `http://codexcnc.com/` 自动跳转到 `https://codexcnc.com/`
8. 保留 `8092 ssl http2` server block使旧访问地址 `https://codexcnc.com:8092/` 仍然可用。
9.`443``8092` 两个 server block 中均保留静态站点根目录 `/var/www/codexcnc-8092`,以及 `/wasm-port/``/configs/``/linuxcnc/``/working_run/` 静态资源规则。
10. 执行 `sudo nginx -t`,配置语法检查通过。
11. 执行 `sudo systemctl reload nginx`Nginx 热重载成功。
12. 执行 `curl -I https://codexcnc.com/`,确认标准 HTTPS 地址返回 `HTTP/2 200`
13. 执行 `curl -I http://codexcnc.com/`,确认 HTTP 地址返回 `301 Moved Permanently`,跳转目标为 `https://codexcnc.com/`
14. 执行 `curl -I https://codexcnc.com:8092/`,确认 8092 HTTPS 地址仍返回 `HTTP/2 200`
15. 使用 `openssl s_client -connect codexcnc.com:443 -servername codexcnc.com` 检查 443 证书,确认主体为 `CN=codexcnc.com`,签发方为 Let's Encrypt证书有效期为 `Jul 7 2026 GMT``Oct 5 2026 GMT`
16. 使用 Playwright 访问 `https://codexcnc.com/`,返回状态为 `200`,页面标题为 `Web XYZBC TRT RTCP Simulation`,当前 URL 为 `https://codexcnc.com/``#app` 文本长度为 `2144`,按钮数量为 `73`,未捕获 console error 或 pageerror。
### 结论
已修复默认 HTTPS 地址的证书问题。现在 `https://codexcnc.com/` 使用 Let's Encrypt 为 `codexcnc.com` 签发的有效证书并返回网站页面,`http://codexcnc.com/` 会跳转到标准 HTTPS 地址,`https://codexcnc.com:8092/` 继续可用。浏览器自动化验证通过,无前端错误。
## 2026-07-07 08:54 EDT
### 提问
用户要求通过 HTTPS 方式将网站发布到服务器 `82.156.24.101`SSH 用户为 `ubuntu`,使用端口 `8092`,域名为 `codexcnc.com`
### 执行过程
1. 读取项目根目录结构,确认当前仓库路径为 `/home/mes123456/cnc_wams`,并检查到应用目录为 `web-rtcp-5axis-xyzbc-trt-sim-plan/app`
2. 查看 `app/package.json`,确认构建命令为 `npm run build`,构建脚本为 `node scripts/build-static.mjs`,静态发布产物目录为 `web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist`
3. 查看 `git status --short`,确认仓库已有多项未提交修改和未跟踪文件;本次任务只进行发布相关操作,不回退或整理这些既有改动。
4. 执行 `npm run build`,构建通过,输出 `gmoccapy_static_build=ok`
5. 检查域名解析,确认 `codexcnc.com` 已解析到 `82.156.24.101`
6. 确认本机存在 `sshpass`,随后使用用户提供的 SSH 信息登录远端服务器。
7. 在远端服务器检查系统信息、端口监听、Nginx、certbot、证书和 `/var/www``/etc/nginx/sites-*` 目录。确认服务器为 Ubuntu 24.04Nginx 处于 active 状态certbot 已安装。
8. 检查远端端口监听,发现 `8092` 已由 Nginx 监听,旧配置文件为 `/etc/nginx/sites-enabled/web-rtcp-5axis-sim`,旧站点根目录为 `/var/www/web-rtcp-5axis-sim`,旧证书使用 `/etc/letsencrypt/live/82.156.24.101/`
9. 通过 `curl -k -I https://82.156.24.101:8092/` 验证旧 8092 站点当前可返回 `HTTP/2 200`,因此本次切换前先备份旧站点和旧配置。
10. 在远端创建 `/var/www/codexcnc-8092``/var/backups/codexcnc-8092`
11. 将旧站点目录 `/var/www/web-rtcp-5axis-sim` 打包备份到 `/var/backups/codexcnc-8092/web-rtcp-5axis-sim.20260707204921.tgz`
12. 将旧 Nginx 配置 `/etc/nginx/sites-available/web-rtcp-5axis-sim` 备份到 `/var/backups/codexcnc-8092/web-rtcp-5axis-sim.nginx.20260707204921.conf`
13. 使用 `scp -r` 上传本地 `web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/.` 到远端 `/var/www/codexcnc-8092/`
14. 设置远端新站点目录权限:目录属主为 `www-data:www-data`,目录权限为 `755`,文件权限为 `644`
15. 执行 `sudo certbot certonly --webroot -w /var/www/letsencrypt -d codexcnc.com --non-interactive --agree-tos --register-unsafely-without-email --keep-until-expiring`,证书签发成功。
16. 证书保存路径为 `/etc/letsencrypt/live/codexcnc.com/fullchain.pem`,私钥路径为 `/etc/letsencrypt/live/codexcnc.com/privkey.pem`,证书有效期到 `2026-10-05 11:53:16+00:00`certbot 已配置自动续期任务。
17. 新增远端 Nginx 配置 `/etc/nginx/sites-available/codexcnc-8092``80` 端口用于 `codexcnc.com` 的 ACME challenge 和跳转,`8092 ssl http2` 用于 HTTPS 静态站点,站点根目录为 `/var/www/codexcnc-8092`
18. 新配置包含 `wasm``js/mjs``css``html``json``svg``png``pdf` 等类型映射,并保留 `/wasm-port/``/configs/``/linuxcnc/``/working_run/` 的静态文件访问规则。
19. 将旧的 `/etc/nginx/sites-enabled/web-rtcp-5axis-sim` 启用链接先改名时发现仍在 `sites-enabled` 中会被 Nginx 继续加载,因此进一步创建 `/etc/nginx/sites-disabled` 并将该链接移出启用目录。
20. 启用新链接 `/etc/nginx/sites-enabled/codexcnc-8092 -> /etc/nginx/sites-available/codexcnc-8092`
21. 执行 `sudo nginx -t`,配置语法检查通过。
22. 执行 `sudo systemctl reload nginx`Nginx 热重载成功。
23. 通过 `curl -I https://codexcnc.com:8092/` 验证新站点返回 `HTTP/2 200`,入口文件 `index.html` 正常返回。
24. 通过 `curl -I http://codexcnc.com/` 验证 80 端口返回 `301 Moved Permanently`,跳转目标为 `https://codexcnc.com:8092/`
25. 使用 `openssl s_client -connect codexcnc.com:8092 -servername codexcnc.com` 检查证书,确认主体为 `CN=codexcnc.com`,签发方为 Let's Encrypt证书有效期为 `Jul 7 2026 GMT``Oct 5 2026 GMT`
26. 通过 `curl -I https://codexcnc.com:8092/src/main.js` 验证主模块返回 `HTTP/2 200`,类型为 `text/javascript`
27. 通过 `curl -I https://codexcnc.com:8092/src/styles/axis.css` 验证 CSS 返回 `HTTP/2 200`,类型为 `text/css`
28. 通过 `curl -I https://codexcnc.com:8092/wasm-port/tools/source-manifest.txt` 验证 WASM 相关目录下资源可访问。
29. 使用 Playwright 访问 `https://codexcnc.com:8092/`,返回状态为 `200`,页面标题为 `Web XYZBC TRT RTCP Simulation``#app` 文本长度为 `2144`,页面按钮数量为 `73`,未捕获 console error 或 pageerror。
30. 再次检查远端 Nginx 加载配置,确认 `8092` 只由 `codexcnc-8092` 配置加载,旧 `web-rtcp-5axis-sim` 链接已移到 `/etc/nginx/sites-disabled/`
31. 使用 `sudo certbot certificates -d codexcnc.com` 确认证书名称为 `codexcnc.com`,状态为 `VALID: 89 days`
### 结论
已完成 `codexcnc.com``82.156.24.101:8092` 的 HTTPS 发布。当前访问地址为 `https://codexcnc.com:8092/`,返回 `HTTP/2 200`,证书为 Let's Encrypt 针对 `codexcnc.com` 签发,浏览器自动化加载验证通过且无前端错误。旧 8092 站点目录和 Nginx 配置已备份,旧启用链接已移到 `/etc/nginx/sites-disabled/`,新站点目录为 `/var/www/codexcnc-8092`Nginx 配置为 `/etc/nginx/sites-available/codexcnc-8092`
## 2026-07-03xyzbc-trt G 代码执行过程 50ms 采样与运行状态补强
### 用户提问
@@ -2397,3 +2504,608 @@ Pause 修复已提交并推送到云仓库 `master`,提交号为 `58854f9`。
### 结论
全部剩余非 ignored 改动已统一提交并推送到云仓库 `master`,提交号为 `6b937a0`。推送成功后工作区已干净ignored 的依赖、构建大目录和本地 LinuxCNC 源码目录未纳入提交。
## 2026-07-05 22:21 EDT
### 提问
用户要求:对标 `/home/mes123456/cnc_wams/linuxcnc源程序``xyzbc-trt`,验证 Web 仿真系统 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的功能流程:解除 `ESTOP``上电``Home All``Run`、暂停 5 秒、继续执行 10 秒、再次暂停 5 秒、继续执行 10 秒,并通过 50 毫秒截屏方式验证。
### 执行过程
1. 读取工作目录 `/home/mes123456/cnc_wams`,确认 AGENTS.md 要求每次执行完毕后将完整过程日志用中文追加到 `gptlog-process/gpdlog.md`
2. 检查用户给出的 LinuxCNC 源码路径,发现字面路径 `/home/mes123456/cnc_wams/linuxcnc源程序` 不存在;工作区存在 `/home/mes123456/cnc_wams/linuxcnc`,其中包含 `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini``xyzbc-trt.xml``demos/xyzbc_switchkins.ngc``remap_subs/xyzbc_switchkins_sub.ngc``remap_subs/helix_bc.ngc` 等 xyzbc-trt 对标文件,因此按该目录作为 LinuxCNC 源码对标来源继续验证。
3. 检查 Web 仿真项目结构与脚本:
- 查看 `web-rtcp-5axis-xyzbc-trt-sim-plan/app/package.json`,确认项目使用 Playwright已有 `dev``smoke:browser``evidence:web` 等脚本。
- 查看已有 `tools/capture-full-gcode-process-frames.mjs`,确认项目已有按照 50ms 样本推进截图的工具。
- 查看 `tests/browser/xyzbc_trt_browser_smoke.html``app/src/ui/axis-shell.js``app/src/state/store.js`,确认 AXIS 界面按钮和状态动作包括 `data-action="estop"``data-action="power"``data-action="home-all"``data-action="run"`,暂停/继续按钮为 `data-tool-id="tbtn_pause"`,并且暂停/继续对应 LinuxCNC 风格的 `PAUSE``RESUME``PAUSE_RESUME` 状态流。
4. 为满足本次指定的真实操作时序,新增专用验证脚本:
- 文件:`web-rtcp-5axis-xyzbc-trt-sim-plan/tools/verify-estop-power-home-run-pause-50ms.mjs`
- 脚本通过本地静态 HTTP 服务打开 `app/index.html`,等待 INI、机床文件、解释器和 task/HAL runtime 就绪。
- 脚本加载 LinuxCNC xyzbc-trt 示例程序 `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc`
- 脚本先确保页面处于 ESTOP 状态,然后通过 DOM 点击方式执行:解除 ESTOP、上电、Home All、Run、第一次暂停、保持 5 秒、第一次继续执行、保持 10 秒、第二次暂停、保持 5 秒、第二次继续执行、保持 10 秒。
- 脚本在流程开始后启动 50ms 间隔截图循环,截图保存到带时间戳的目录,并在 manifest 中记录每次点击、等待保持、运行状态、活动行、样本索引、LinuxCNC G-code 扩展执行信息、最终状态和对标源码路径。
5. 执行验证命令:
- 工作目录:`/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan`
- 命令:`node tools/verify-estop-power-home-run-pause-50ms.mjs`
6. 验证脚本运行完成,输出:
- `verification_status=passed`
- 截图目录:`/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T021839Z`
- 50ms 截图帧数:`captured_frames=388`
- 采样周期:`sample_period_ms=50`
- manifest`/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T021839Z/manifest.json`
7. 抽查 manifest 和截图目录:
- manifest 状态为 `passed`
- manifest 记录 `capturedFrameCount=388`,截图目录中 PNG 文件总数为 389其中包括 388 张 50ms 连续帧和 1 张初始 ESTOP 快照。
- 截图目录大小约 90M。
- WebGL/canvas 预览渲染检查通过。
8. 抽查关键流程状态:
- 初始设置:`runState=estopped``mode=manual``interpState=idle`
- 解除 ESTOP 后:`runState=idle``taskState=estop-reset``estopActive=false`
- 上电后:`powerOn=true``taskState=on`
- Home All 后:`allHomed=true``mode=manual`
- Run 后:`runState=running``mode=auto``interpState=reading``taskPaused=false`
- 第一次暂停后:`runState=paused``interpState=paused``taskPaused=true`,样本索引约为 50。
- 第一次暂停保持阶段通过,记录保持时间约 7351ms期间状态保持暂停。
- 第一次继续执行后:`runState=running``interpState=reading``taskPaused=false`,样本索引从 51 继续推进。
- 第一次继续执行保持阶段通过,记录保持时间约 11356ms样本索引推进到约 256。
- 第二次暂停后:`runState=paused``interpState=paused``taskPaused=true`,样本索引约为 314。
- 第二次暂停保持阶段通过,记录保持时间约 5168ms期间状态保持暂停。
- 第二次继续执行后:`runState=running``interpState=reading``taskPaused=false`,样本索引从 315 继续推进。
- 第二次继续执行保持阶段通过,记录保持时间约 10106ms样本索引推进到约 487。
9. 抽查最终状态:
- `runState=complete`
- `taskState=on`
- `powerOn=true`
- `estopActive=false`
- `allHomed=true`
- `mode=auto`
- `interpState=idle`
- `taskPaused=false`
- `programExecutionSourceMode=linuxcnc-task-motion-hal-wasm`
- `samplePeriodMs=50`
- `sampleCount=1300`
- `gcodeExecutionProcess.status=ok`
- `executionStepCount=128`
- `motionStepCount=29`
10. 执行 `git status --short`,确认本轮新增内容包括专用验证脚本和本次截图证据目录;未修改 Web 应用运行逻辑。
### 结论
本次对标验证通过。Web 仿真系统在 xyzbc-trt 程序下成功完成解除 ESTOP、上电、Home All、Run、暂停 5 秒、继续执行 10 秒、再次暂停 5 秒、继续执行 10 秒的完整流程;暂停阶段状态保持为 `paused`,继续执行阶段状态恢复为 `running` 并推进样本索引;最终程序完成。验证证据已保存为 50ms 截图序列和 manifest路径为 `web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T021839Z/`
## 2026-07-05 22:25 EDT
### 提问
用户询问:比对图片对应的 `ESTOP -> 上电 -> Home All -> Run -> 暂停` 是否合理。
### 执行过程
1. 读取本次验证生成的 manifest`web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T021839Z/manifest.json`
2. 从 manifest 中提取事件链和状态转移,定位关键帧:
- `frame-00000-t000055ms-estopped.png`ESTOP 状态,`runState=estopped``taskState=estop``mode=manual``interpState=idle`,速度 0。
- `frame-00002-t000899ms-idle.png`:解除 ESTOP 后,`runState=idle``taskState=estop-reset``mode=manual``interpState=idle`,速度 0。
- `frame-00004-t001770ms-idle.png`:上电后,`runState=idle``taskState=on``mode=manual``interpState=idle`,速度 0。
- `frame-00006-t002623ms-idle.png`Home All 后的空闲帧,`runState=idle``taskState=on``mode=manual``interpState=idle`
- `frame-00007-t002797ms-running.png`Run 后,`runState=running``taskState=on``mode=auto``interpState=reading``taskPaused=false`
- `frame-00008-t005679ms-paused.png`:第一次暂停后,`runState=paused``taskState=on``mode=auto``interpState=paused``taskPaused=true`,样本索引 50。
3. 使用 ImageMagick `montage` 将以上 6 张关键帧合成为对照图:
- `web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T021839Z/key-sequence-estop-power-home-run-pause.jpg`
4. 视觉检查合成图:
- 第一格底部状态栏显示 `ESTOP`,与 ESTOP 状态一致。
- 第二格底部状态栏显示 `OFF`,与解除 ESTOP 后机器未上电一致。
- 第三格底部状态栏显示 `ON`,与上电后状态一致。
- 第四格仍为 `ON/manual/identity` 空闲界面,结合 manifest 的 `allHomed=true` 可作为 Home All 后状态;仅凭图片文字不明显显示 “homed”需要依赖 manifest 状态确认。
- 第五格显示运行中,右侧 kinematics 为 `1:XYZBC`,底部速度出现非零值,与 Run 后进入 auto/reading/running 一致。
- 第六格为暂停后状态manifest 明确为 `runState=paused``interpState=paused``taskPaused=true`;画面底部仍为 `ON`,右侧显示 `0:IDENTITY`,这是暂停点发生在样本 50、对应 `helix_bc.ngc` 第 16 行 `g0b#<b>c#<c>` 后的状态表现图片本身不直接显示“PAUSED”大字需要以 manifest 为准。
### 结论
图片与状态链总体合理。`ESTOP -> 解除 ESTOP/OFF -> 上电/ON -> Home All 后 ON/manual -> Run/running -> Pause/paused` 的顺序和 manifest 状态一致。需要注意的是Home All 和 Pause 两个节点在截图画面上不如状态 JSON 直观Home All 主要通过 `allHomed=true` 证明Pause 主要通过 `runState=paused``interpState=paused``taskPaused=true` 证明,截图底部仍显示 `ON` 属于 LinuxCNC/AXIS 风格状态栏显示,不代表未暂停。
## 2026-07-05 22:42 EDT
### 提问
用户反馈:程序执行过程中点击“暂停程序”按钮,程序没有暂停执行。
### 执行过程
1. 将用户反馈按实际缺陷处理,重新检查 `Pause` 相关代码路径:
- AXIS 工具栏暂停按钮:`app/src/ui/axis-shell.js``data-tool-id="tbtn_pause"`,动作 `pause-resume`
- AXIS 菜单暂停:`data-menu-command="pause"`,动作 `PAUSE`
- gmoccapy 底部暂停按钮:`app/src/ui/gmoccapy-shell.js``data-action="PAUSE"`,动作 `PAUSE`
- 状态机处理:`app/src/state/store.js``PAUSE``PAUSE_RESUME`、task/HAL 状态回写 `TASK_HAL_STATUS_APPLIED`
2. 复查此前 50ms 截图 manifest发现一个关键不一致
- 点击暂停后 `runState=paused``interpState=paused``taskPaused=true`,样本索引冻结在 50。
- 但暂停保持期间后续帧的 `currentVelocity` 又出现非零值,例如旧 manifest 中暂停帧 `frame-00009``frame-00012` 显示 `currentVelocity=203.7696`
- 这会导致用户从界面速度/监控面板判断“程序还在执行”,即使内部状态和样本索引已经暂停。
3. 定位原因:
- `PAUSE` 分支在 task/HAL 路径下会先把 `feed.currentVelocity` 置 0但后续 task/HAL 状态回写 `applyTaskHalStatusPatch` 仍可能把 `ui.currentVelocity` 合成为非零速度。
- 普通非 task/HAL 路径下,`PAUSE` 分支只设置 `runState=paused``machine.interpState=paused`,没有同步清零 `feed.currentVelocity`
- `programRuntimeFeedback.currentVelocityMmPerMin``requestedVelocityMmPerMin` 没有在暂停态统一清零,监控面板仍可能显示运动速度。
4. 修改 `app/src/state/store.js`
-`PAUSE` 分支中,无论 task/HAL 路径还是普通路径,都将 `feed.currentVelocity` 设置为 0。
- 新增 `zeroProgramRuntimeVelocity(feedback)`,在暂停时保留当前行、样本、姿态等信息,但将 `currentVelocityMmPerMin``requestedVelocityMmPerMin``distanceToGo``dtg``activeDepth` 清零。
-`applyTaskHalStatusPatch` 中,如果解释器状态为 `paused` 或 motion 报告 paused则合成出的 `currentVelocity` 强制为 0避免 task/HAL 后续状态回写覆盖暂停速度。
-`createTaskHalRuntimeFeedback` 中,如果状态为 paused`currentVelocityMmPerMin``requestedVelocityMmPerMin` 都强制为 0。
5. 修改 `tests/node/verify_xyzbc_trt_web_app.mjs`
-`PAUSE_RESUME` 和直接 `PAUSE` 的断言中增加 `feed.currentVelocity === 0`,覆盖用户看到的暂停后速度不归零问题。
6. 执行测试:
- `node tests/node/verify_xyzbc_trt_web_app.mjs`:通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `node tests/node/verify_run_feedback_loop.mjs`:通过,输出 `run_feedback_status_loop_smoke=ok``run_ready_sequence_smoke=ok`
- `git diff --check`:通过,无空白错误。
7. 重新执行浏览器 50ms 截图验证:
- 命令:`node tools/verify-estop-power-home-run-pause-50ms.mjs`
- 输出:`verification_status=passed`
- 新截图目录:`web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T024020Z`
- 新 manifest`web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T024020Z/manifest.json`
- 截图帧数397采样周期 50ms。
8. 抽查新 manifest
- 所有 paused 帧的速度值只有 `0``pausedNonZero=0`
- 第一次暂停后:`runState=paused``sampleIndex=50``currentVelocity=0``taskPaused=true`
- 第一次暂停保持 5 秒后:仍为 `runState=paused``sampleIndex=50``currentVelocity=0``taskPaused=true`,说明样本索引冻结且速度归零。
- 第二次暂停点样本索引为 321暂停期间同样保持速度 0。
### 结论
用户反馈成立:旧逻辑中点击“暂停程序”后,内部暂停状态和样本索引已经冻结,但速度反馈可能被后续 task/HAL 状态回写恢复为非零,导致界面表现为仍在执行。已修复为暂停时统一冻结执行反馈并清零速度;重新运行 50ms 截图验证后paused 帧无非零速度,暂停保持期间样本索引不推进。
## 2026-07-05 23:00 EDT
### 提问
用户继续反馈点击暂停按钮后刀具还在运行Position 位置还在变化。
### 执行过程
1. 将反馈继续按缺陷处理重点从“速度是否归零”扩展到“刀具位置、Position/DRO、axisPose 是否冻结”。
2. 检查渲染与状态来源:
- `app/src/state/store.js``applyTaskHalStatusPatch` 会根据 task/HAL 状态回写 `axisPose`
- `app/src/visualization/five-axis-scene.js``executionToolPosition` 优先使用 `programUiExecution.tcp``programRuntimeFeedback.tcp``programRuntimeFeedback.axisPose`
- `app/src/runtime/vismach-model-state.js``resolveAxisPose` 优先使用 `programRuntimeFeedback.axisPose`,否则使用 `taskHalStatus.ui.axisPose`
- AXIS 右侧 `Position` 面板来自 `linuxCncProcessMonitor.axes.joint`,最终来自 `state.axisPose`
3. 定位原因:
- 上一轮修复已经让暂停状态和速度为 0但 paused 状态下的 task/HAL 状态回写仍可能携带新的 `ui.axisPose`
- 如果 paused 回写继续把新的 `axisPose``programRuntimeFeedback.axisPose``programUiExecution.tcp` 写进 UI刀具和 Position 仍会变化。
- Vismach 还有一条 fallback如果没有合适的 `programRuntimeFeedback.axisPose`,会读取 `taskHalStatus.ui.axisPose`,也可能绕过冻结位置。
4. 修改 `app/src/state/store.js`
-`applyTaskHalStatusPatch` 中,如果解释器状态为 `paused` 或 motion 报告 paused`axisPose` 不再来自 task/HAL 新状态,而是使用当前 `state.axisPose`
- paused 状态下的 `idleRuntimeFeedback` 显式写入冻结的 `axisPose``tcp`,确保刀具渲染使用暂停瞬间的位置。
- 保留上一轮速度修复paused 状态下 `currentVelocity``currentVelocityMmPerMin``requestedVelocityMmPerMin` 均为 0。
5. 修改 `app/src/runtime/vismach-model-state.js`
- `resolveAxisPose``runState=paused``machine.interpState=paused``machine.taskPaused=true` 时,优先返回 `state.axisPose`,不再从 `taskHalStatus.ui.axisPose` 取可能继续变化的底层位置。
6. 增强 `tools/verify-estop-power-home-run-pause-50ms.mjs`
- 在两段“暂停保持 5 秒”中加入 `freezePosition` 验证。
- 每 100ms 比对暂停开始时和当前的 `axisPose``dro`、canvas `threeToolhead`、Vismach pins。
- 如果 Position 或刀具位置变化,脚本会直接失败并指出具体字段。
7. 执行验证:
- `node tests/node/verify_xyzbc_trt_web_app.mjs`:通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `node tests/node/verify_run_feedback_loop.mjs`:通过,输出 `run_feedback_status_loop_smoke=ok``run_ready_sequence_smoke=ok`
- `git diff --check`:通过,无空白错误。
8. 重新执行浏览器 50ms 截图验证:
- 命令:`node tools/verify-estop-power-home-run-pause-50ms.mjs`
- 输出:`verification_status=passed`
- 新截图目录:`web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T025722Z`
- 新 manifest`web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T025722Z/manifest.json`
- 截图帧数372采样周期 50ms。
9. 抽查新 manifest
- `status=passed`
- paused 帧非零速度数量为 0。
- 第一次暂停保持 5 秒:`runState=paused``sampleIndex=54``currentVelocity=0`,冻结 `axisPose` 为 X=17.810095691284364、Y=9.30122682754529、Z=12.808225907624593、B=20、C=45DRO 与该 axisPose 一致canvas toolhead 固定。
- 第二次暂停保持 5 秒:`runState=paused``sampleIndex=323``currentVelocity=0`,冻结 `axisPose` 为 X=1.33333、Y=0、Z=10、B=0、C=0DRO 与该 axisPose 一致canvas toolhead 固定。
- 增强脚本未报告任何 `axisPose``dro` 或 canvas toolhead 变化。
### 结论
用户反馈成立:仅清零速度还不够,暂停时 task/HAL 的后续状态回写仍可能让刀具位置和 Position 继续变化。已修复为 paused 状态下冻结 `axisPose`、DRO、runtime feedback TCP/axisPose 和 Vismach 位置来源;增强后的 50ms 浏览器验证已通过,暂停保持期间 Position 和刀具位置不再变化。
## 2026-07-05 23:13 EDT
### 提问
用户再次反馈:点击截图红框中的“暂停按钮”(蓝色双竖线按钮,`tbtn_pause`后仍没有暂停刀具还在运行Position 位置还在变化;要求通过执行过程 JSON 追踪 Position 变化,发现错误并修改。
### 执行过程
1. 根据用户最新截图确认红框按钮为 AXIS 工具栏的蓝色双竖线按钮:
- DOM 标识:`data-tool-id="tbtn_pause"`
- 动作:`data-action="pause-resume"`
- 代码位置:`app/src/ui/axis-shell.js`
2. 检查当前工作区状态和残留进程:
- 未发现残留 Playwright/Chromium 验证进程。
- 当前存在 `app/src` 修复、`app/dist` 旧构建、截图证据和日志改动。
3. 发现一个重要线索:
- 手动页面很可能打开的是 `app/dist/index.html`
- 之前修复主要在 `app/src` 中完成,`app/dist/src/...` 仍是旧构建,尚未包含 paused 状态下的速度/Position 冻结修复。
4. 新增专用 JSON 追踪脚本:
- 文件:`web-rtcp-5axis-xyzbc-trt-sim-plan/tools/trace-pause-position-json.mjs`
- 默认打开 `app/dist/index.html`
- 自动加载 `xyzbc_switchkins.ngc`,进入上电、回零、运行状态。
- 真实点击双竖线暂停按钮 `[data-tool-id="tbtn_pause"]`
- 暂停后每 100ms 记录 3.5 秒的 JSON 样本,包括 `runState``interpState``sampleIndex``currentVelocity``axisPose``dro``programRuntimeFeedback.axisPose``programUiExecution.joint/tcp`、canvas `threeToolhead` 和按钮状态。
5. 首次在旧 `app/dist` 上运行追踪脚本:
- 命令:`node tools/trace-pause-position-json.mjs`
- 输出状态:`pause_position_status=failed-position-changed`
- Trace`web-rtcp-5axis-xyzbc-trt-sim-plan/working/pause-position-traces/pause-position-20260706T031130Z/trace.json`
- 追踪分析:`changedFields=["currentVelocity"]`
6. 分析旧 dist 的失败 JSON
- 暂停开始:`runState=paused``sampleIndex=47``currentVelocity=203.7696`
- 暂停 3.5 秒后:`runState=paused``sampleIndex=47``currentVelocity=203.7696`
- `axisPose` 前后保持一致X=17.260628294473975、Y=12.226512780815126、Z=12.35245303980423、B=15.8621、C=35.6897。
- `dro` 前后保持一致。
- canvas `threeToolhead` 前后保持一致。
- 因此 JSON 追踪显示Position/刀具坐标没有继续变化,但旧 dist 的速度反馈未归零,界面表现仍像刀具在运行。
7. 执行静态构建,把 `app/src` 修复同步到 `app/dist`
- 命令:`npm run build`
- 工作目录:`web-rtcp-5axis-xyzbc-trt-sim-plan/app`
- 输出:`gmoccapy_static_build=ok`
8. 构建后检查 dist
- `app/dist/src/state/store.js` 已包含 `zeroProgramRuntimeVelocity`、paused 状态速度清零、paused 状态冻结 `axisPose`
- `app/dist/src/runtime/vismach-model-state.js` 已包含 paused 状态优先返回 `state.axisPose` 的逻辑。
9. 在新 `app/dist` 上再次运行同一 JSON 追踪脚本:
- 命令:`node tools/trace-pause-position-json.mjs`
- 输出状态:`pause_position_status=passed-position-frozen`
- Trace`web-rtcp-5axis-xyzbc-trt-sim-plan/working/pause-position-traces/pause-position-20260706T031219Z/trace.json`
- 追踪分析:`changedFields=[]`
10. 分析新 dist 的通过 JSON
- 暂停开始:`runState=paused``sampleIndex=48``currentVelocity=0`
- 暂停 3.5 秒后:`runState=paused``sampleIndex=48``currentVelocity=0`
- `axisPose` 前后保持一致X=17.46662953600752、Y=11.844901245837626、Z=12.434434732959277、B=16.5517、C=37.2414。
- `dro` 前后保持一致。
- canvas `threeToolhead` 前后保持一致。
- 双竖线按钮状态为 pausedtitle 为 `Resume program`
11. 执行验证:
- `node tests/node/verify_xyzbc_trt_web_app.mjs`:通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `node tests/node/verify_run_feedback_loop.mjs`:通过,输出 `run_feedback_status_loop_smoke=ok``run_ready_sequence_smoke=ok`
- `npm run build`:通过,输出 `gmoccapy_static_build=ok`
- `git diff --check`:通过,无空白错误。
### 结论
通过执行过程 JSON 追踪确认:用户点击的确实是蓝色双竖线暂停按钮 `tbtn_pause`。旧 `app/dist` 页面未包含最新暂停修复,导致暂停后 `currentVelocity` 仍保持非零界面表现为刀具仍在运行JSON 同时显示 Position/DRO/canvas toolhead 实际没有继续变化。已执行构建同步 `app/src` 修复到 `app/dist`,再次追踪后 `sampleIndex``axisPose`、DRO、canvas toolhead 全部冻结,`currentVelocity=0`,暂停按钮状态正确切换为 `Resume program`
## 2026-07-05 23:34 EDT
### 提问
用户要求对标 `/home/mes123456/cnc_wams/linuxcnc源程序` 的 xyzbc-trt解决项目 Web 仿真系统 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 中点击红框内暂停按钮后 G 代码执行仍未暂停、刀具仍在运行的问题;要求通过保存 G 代码真实执行过程 JSON、追踪 Position 仍在变化来发现错误并修改,并且真实使用界面直接操作测试。
### 执行过程
1. 读取仓库规则:确认 `AGENTS.md` 要求每次执行结束后追加中文日志到 `gptlog-process/gpdlog.md`,时间戳精确到分钟。
2. 检查工作区状态:发现已有未提交改动包含 `app/src/state/store.js``app/src/runtime/vismach-model-state.js`、对应 `dist` 文件、`tests/node/verify_xyzbc_trt_web_app.mjs`、新增 Playwright 追踪脚本和截图证据目录;未回退这些既有改动。
3. 搜索并阅读暂停相关代码:重点检查 `PAUSE``PAUSE_RESUME``RESUME``TASK_HAL_STATUS_APPLIED``applyTaskHalStatusPatch``createTaskHalRuntimeFeedback``buildVismachModelState`、AXIS/gmoccapy 按钮事件映射。
4. 对照 LinuxCNC 源码:阅读 `/home/mes123456/cnc_wams/linuxcnc/src/emc/usr_intf/axis/scripts/axis.py``task_pause/task_pauseresume`,确认 AXIS 按钮发送 `AUTO_PAUSE`;阅读 `/home/mes123456/cnc_wams/linuxcnc/src/emc/task/emctaskmain.cc`,确认 `EMC_TASK_PLAN_PAUSE` 调用 `emcTrajPause()`,并设置 `interpState=PAUSED``task_paused=1`。Web 侧应在这个状态下停止采样推进并保持最后位置。
5. 分析已有失败/通过 trace旧失败 trace `pause-position-20260706T031130Z/trace.json` 中暂停后 `axisPose/dro/runtimeAxisPose/uiExecution/canvasToolhead` 已冻结,但 `currentVelocity=203.7696` 未归零;较新的 trace `pause-position-20260706T031219Z/trace.json` 显示 `currentVelocity=0``changedFields=[]`
6. 在修改前重新真实点击界面验证一次:执行 `node web-rtcp-5axis-xyzbc-trt-sim-plan/tools/trace-pause-position-json.mjs`,脚本打开 `app/dist/index.html`、加载真实 xyzbc_switchkins G 代码、上电回零、点击 `[data-tool-id="tbtn_pause"]`,输出 `pause_position_status=passed-position-frozen`,生成 `working/pause-position-traces/pause-position-20260706T032612Z/trace.json`
7. 发现并修正一个剩余风险:`buildVismachModelState()` 先读取 `programRuntimeFeedback.axisPose` 再判断 paused导致暂停时如果运行反馈仍有姿态Vismach 可能绕过冻结位置。修改 `app/src/runtime/vismach-model-state.js``app/dist/src/runtime/vismach-model-state.js`,将 paused 判断前置,使暂停态优先使用冻结后的 `state.axisPose`
8.`tests/node/verify_xyzbc_trt_web_app.mjs` 增加断言:构造 `runState=paused``programRuntimeFeedback.axisPose` 为不同数值的状态,验证 Vismach pins 仍使用 `state.axisPose`,防止后续回归。
9. 保留并验证暂停状态修复:`app/src/state/store.js``app/dist/src/state/store.js` 在 PAUSE 中把 `feed.currentVelocity` 置 0并通过 `zeroProgramRuntimeVelocity()``programRuntimeFeedback.currentVelocityMmPerMin/requestedVelocityMmPerMin/distanceToGo/dtg/activeDepth` 归零;`applyTaskHalStatusPatch()` 在 paused 状态使用当前 `state.axisPose` 而不是新的 task/HAL status 覆盖位置;`createTaskHalRuntimeFeedback()` 在 paused 状态输出速度 0。
10. 运行构建:`npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build`,输出 `gmoccapy_static_build=ok`
11. 运行 Node smoke`npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:node`,输出 `xyzbc_trt_web_app_smoke=ok`
12. 再次真实点击界面并保存执行过程 JSON执行 `node web-rtcp-5axis-xyzbc-trt-sim-plan/tools/trace-pause-position-json.mjs`,输出 `pause_position_status=passed-position-frozen`trace 为 `working/pause-position-traces/pause-position-20260706T032836Z/trace.json``position_changed=false``changed_fields=`
13. 抽取最新 trace 关键结果:暂停开始 `sampleIndex=46``currentVelocity=0``axisPose={x:17.045851125409683,y:12.606296161544156,z:12.268669711823136,b:15.1724,c:34.1379}`;暂停 3.5 秒后 `sampleIndex=46``currentVelocity=0`、同一 `axisPose`、同一 DRO、同一 `runtimeAxisPose`、同一 `uiJoint`、同一 `canvasToolhead={x:0.01,y:0.02,z:0.01}`
14. 运行真实界面长流程 50ms 验证:执行 `node web-rtcp-5axis-xyzbc-trt-sim-plan/tools/verify-estop-power-home-run-pause-50ms.mjs`,脚本用 Playwright 真实点击解除 ESTOP、上电、Home All、Run、第一次暂停、继续、第二次暂停、继续输出 `verification_status=passed`,截图/manifest 目录为 `working/screenshots/estop-power-home-run-pause-50ms-20260706T032947Z`,捕获 372 帧,采样周期 50ms。
15. 检查长流程 manifest第一次暂停保持约 5.2 秒和第二次暂停保持约 5.46 秒均为 `runState=paused``interpState=paused``taskPaused=true``currentVelocity=0`,冻结位置断言全部通过;继续执行阶段速度恢复非零,说明暂停/继续切换正常。
16. 运行 `git diff --check`,无空白错误。
### 结论
已定位问题根因在 Web 暂停态仍可能使用运行反馈或 task/HAL status 中的运动数据驱动速度/姿态显示。已修复暂停处理:按 LinuxCNC 的 `EMC_TASK_PLAN_PAUSE` 语义将 `interpState/taskPaused/runState` 置为暂停,同时冻结 `axisPose`归零速度和剩余距离Vismach 模型在暂停态优先使用冻结位置。真实界面点击红框内蓝色双竖线暂停按钮后,最新 JSON trace 显示 Position、DRO、runtimeAxisPose、uiExecution、canvasToolhead 在暂停保持期间全部不变,`currentVelocity=0`50ms 长流程截图/状态采样也通过两次暂停保持冻结断言。
## 2026-07-05 23:48 EDT
### 提问
用户要求继续测试“运行稳定后点暂停”,确认是否能使用真实页面测试红框内暂停按钮。
### 执行过程
1. 继续沿用真实页面验证方法,不通过 store dispatch 代替用户操作。
2.`web-rtcp-5axis-xyzbc-trt-sim-plan/tools/verify-estop-power-home-run-pause-50ms.mjs` 增加环境变量 `RUN_STABLE_BEFORE_FIRST_PAUSE_MS`:在点击 Run 后先保持运行指定毫秒数,再点击第一次暂停。
3. 在同一脚本中增加 `requirePositionChange` 检查Run 后稳定运行阶段必须检测到 `axisPose`、DRO、canvas toolhead 或 Vismach pins 至少有一个位置字段发生变化,否则测试失败。这样可以证明暂停前刀具确实在运行。
4. 增加 `hasPositionChanged()` helper用于比较 `axisPose`、DRO、canvas toolhead、Vismach pins。
5. 执行真实页面长流程测试命令:`RUN_STABLE_BEFORE_FIRST_PAUSE_MS=10000 node web-rtcp-5axis-xyzbc-trt-sim-plan/tools/verify-estop-power-home-run-pause-50ms.mjs`
6. 脚本通过 Playwright 打开真实页面,依次点击页面按钮:解除 ESTOP、上电、Home All、RunRun 后稳定运行 10000ms之后点击红框内蓝色双竖线暂停按钮保持暂停 5 秒;再继续、第二次暂停、第二次继续。
7. 测试输出:`verification_status=passed`;截图目录:`/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T034449Z`manifest`/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T034449Z/manifest.json`;捕获 444 帧;采样周期 50ms。
8. 抽取 manifest 关键数据Run 后稳定运行阶段持续 11588ms`movedDuringHold=true`,结束时 `runState=running``interpState=reading``taskPaused=false``sampleIndex=208``currentVelocity=996.516`,证明暂停前 Position 确实在变化。
9. 第一次点击暂停结果:耗时 2744ms 后进入 `runState=paused``interpState=paused``taskPaused=true``sampleIndex=269``currentVelocity=0`
10. 第一次暂停保持 5 秒结果:持续 5341ms`sampleIndex` 始终为 269`currentVelocity=0`,冻结位置为 `axisPose={x:27.700760989390186,y:11.778283396460903,z:9.676630009741636,a:0,b:20,c:45}`DRO 同步为相同 XYZBC 和 TCPcanvas toolhead 为 `{x:0.014,y:0.028,z:0.005}`。冻结断言通过。
11. 第二次暂停保持结果:持续 5804ms`sampleIndex=503``currentVelocity=0`,冻结位置为 `axisPose={x:8.314084408372068,y:32.79894232744489,z:1.358641453441094,a:0,b:20,c:45}`DRO/TCP 同步canvas toolhead 为 `{x:-0.016,y:0.029,z:0.007}`。冻结断言通过。
### 结论
已按用户指定的“运行稳定后点暂停”场景使用真实页面测试Run 后先稳定运行约 11.6 秒且检测到 Position 确实变化,然后点击红框内暂停按钮。测试结果显示点击暂停后 G 代码执行进入 paused 状态速度归零sampleIndex 不再推进Position/DRO/canvas toolhead 在暂停保持期间冻结。证据见 manifest 和 444 帧截图目录。
## 2026-07-05 23:56 EDT
### 提问
用户反馈之前给出的两个浏览器 URL 都打不开Chrome 显示 `127.0.0.1 refused to connect`,要求解决页面访问问题。
### 执行过程
1. 判断截图中的 `ERR_CONNECTION_REFUSED` 表示 4174 端口没有 HTTP 服务监听,不是页面路径 404。
2. 检查端口:执行 `ss -ltnp | rg ':4174|:4175' || true`,确认 4174/4175 初始均没有监听。
3. 检查页面文件:确认 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/app/index.html``/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html` 均存在。
4. 首次尝试用 `nohup python3 -m http.server 4174 --bind 127.0.0.1 --directory /home/mes123456/cnc_wams` 后服务未保持curl 仍连接失败,因此改用前台会话确认命令本身可运行。
5. 前台启动 `python3 -m http.server 4174 --bind 127.0.0.1 --directory /home/mes123456/cnc_wams` 后,`curl -I http://127.0.0.1:4174/web-rtcp-5axis-xyzbc-trt-sim-plan/app/index.html` 返回 `HTTP/1.0 200 OK``curl -I http://127.0.0.1:4174/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html` 返回 `HTTP/1.0 200 OK`
6. 为避免服务随会话结束而关闭,测试 `setsid -f python3 -m http.server 4175 --bind 127.0.0.1 --directory /home/mes123456/cnc_wams`,确认 4175 可脱离会话后台监听,并且两个页面 URL 都返回 200。
7. 停止前台会话型 4174 服务后,使用 `setsid -f python3 -m http.server 4174 --bind 127.0.0.1 --directory /home/mes123456/cnc_wams >/tmp/cnc_wams_http_4174.log 2>&1` 启动脱离会话的 4174 固定端口服务。
8. 验证最终状态:`ss -ltnp | rg ':4174'` 显示 `python3` PID 25396 正在监听 `127.0.0.1:4174``curl -I http://127.0.0.1:4174/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html` 返回 `HTTP/1.0 200 OK`
### 结论
已在本机固定启动 HTTP 服务,监听 `127.0.0.1:4174`,根目录为 `/home/mes123456/cnc_wams`。用户现在可以打开 `http://127.0.0.1:4174/web-rtcp-5axis-xyzbc-trt-sim-plan/app/index.html``http://127.0.0.1:4174/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html`;当前 dist 页面已由 curl 验证返回 200。另有 4175 作为备用后台服务。
## 2026-07-06 00:09 EDT
### 提问
用户指出红框中的两个按钮可能被混淆:左侧双竖线按钮和右侧蓝色三角加竖线按钮,反馈暂停按钮仍不好用,要求确认两个按钮功能是否混淆。
### 执行过程
1. 读取 `app/src/ui/axis-shell.js`:确认工具栏渲染中左侧按钮为 `toolButton("tbtn_pause", "pause-resume", ...)`,右侧按钮为 `toolButton("btn_step", "step", "Step")`
2. 读取事件分发:`runAxisCommand()``pause-resume` 分发 `{type:"PAUSE_RESUME"}``step` 分发 `{type:"STEP"}`。因此源码中两个按钮功能不是同一个。
3. 确认 parity 映射:`toolbar-pause-resume` 对标 LinuxCNC `commands.task_pauseresume`,预期 `AUTO_PAUSE/AUTO_RESUME``toolbar-step` 对标 LinuxCNC `commands.task_step`,预期 single step。
4. 使用真实浏览器打开 `http://127.0.0.1:4174/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html`,先尝试直接点击工具栏 ESTOP发现页面初始已是 `estop-reset`,此时再点击 ESTOP 会重新进入急停。由此确认用户操作中的“解除 ESTOP”需要根据状态判断如果已经 reset不应再点急停按钮。
5. 重新执行真实按钮测试:使用系统 Chrome通过 Playwright 打开同一页面,加载真实 `xyzbc_switchkins.ngc`,如果 ESTOP 已解除则跳过急停按钮,点击工具栏上电按钮 `tbtn_on`,执行 Home点击运行按钮 `btn_run`,稳定运行 10 秒后分别测试两个红框按钮。
6. 稳定运行后状态:`runState=running``interpState=reading``taskPaused=false``activeLine=17``sampleIndex=271``currentVelocity=996.516`,按钮 DOM 显示 `tbtn_pause action=pause-resume title=Pause program``btn_step action=step title=Step`
7. 点击左侧双竖线 `tbtn_pause` 后状态:`runState=paused``interpState=paused``taskPaused=true``activeLine=19``sampleIndex=285``currentVelocity=0`,按钮标题变为 `Resume program``data-paused=true`
8. 左侧暂停保持 2500ms 后状态仍为:`runState=paused``interpState=paused``taskPaused=true``sampleIndex=285``currentVelocity=0``axisPose` 未变化,说明左侧按钮确实暂停并冻结。
9. 再次点击左侧双竖线恢复运行,然后点击右侧蓝色三角加竖线 `btn_step`。点击前运行状态为 `runState=running``sampleIndex=368``currentVelocity=203.7696`
10. 点击右侧 `btn_step` 后状态为:`runState=paused``interpState=paused``taskPaused=true``activeLine=17``sampleIndex=381``currentVelocity=0`operatorMessage 为 `task/HAL stepped one cycle`。这证明右侧按钮不是暂停按钮,而是先推进一个 step/cycle 再暂停。
### 结论
两个按钮没有在代码中混淆:左侧双竖线 `tbtn_pause` 是暂停/继续,右侧蓝色三角加竖线 `btn_step` 是步进。真实页面测试显示左侧按钮点击后保持暂停并冻结;右侧按钮点击后会先执行一个步进动作再进入暂停,所以如果把右侧按钮当作暂停,会看到刀具继续/跳动一下,这是步进按钮的预期行为。另需注意页面初始可能已经是 ESTOP reset 状态,此时不应再点击 ESTOP 图标,否则会重新进入急停。
## 2026-07-06 00:18 EDT
### 提问
用户反馈暂停按钮仍不好用,按按钮没有反应,判断应该是执行条件设计不对。
### 执行过程
1. 按“条件设计不对”方向审查代码,不再只检查成功路径。重点查看 `PAUSE_RESUME``PAUSE``RESUME``gateLinuxCncTaskAction()`
2. 发现风险点:`PAUSE_RESUME` 旧逻辑要求 `taskMode` 必须为 `auto/mdi` 才能分发暂停;但 Web 仿真在真实运行中可能已经 `runState=running``interpState=reading`,而界面/状态字段仍显示或滞后为 `mode=manual`。这种状态下点击左侧双竖线按钮会被条件拦住,表现为“按按钮没有反应”。
3. 修改 `app/src/state/store.js``PAUSE_RESUME` 不再因为 `taskMode` 为 manual 而忽略;只要程序实际处于 `runState=running/stepping``interpState!=idle`,就分发 `PAUSE`;只要实际处于 paused就分发 `RESUME`
4. 修改 `app/src/state/store.js`:执行 PAUSE/RESUME 时用 `programControlModeForMachine()` 将程序控制模式修正为 `auto`(保留 mdi并清掉 `manualPanel`,避免暂停后仍显示为手动模式导致下一次继续也被条件挡住。
5. 修改 `app/src/state/linuxcnc-task-policy.js``PAUSE` gate 改为优先尊重实际运行状态,若 `runState=running/stepping``interpState=reading/waiting`,即使 `taskMode` 字段滞后为 manual 也允许暂停;`RESUME` gate 在 `runState=paused` 时允许恢复。
6.`tests/node/verify_xyzbc_trt_web_app.mjs` 增加回归测试:构造 `mode=manual``runState=running``interpState=reading` 的状态,执行 `PAUSE_RESUME` 必须进入 paused且 mode 被修正为 auto、速度为 0再次执行 `PAUSE_RESUME` 必须恢复 running。
7. 运行构建:`npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build`,通过,输出 `gmoccapy_static_build=ok`,已同步到 `app/dist`
8. 运行 Node smoke`npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:node`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
9. 使用真实页面重新验证左侧暂停按钮:执行 `APP_URL_PATH=/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html node web-rtcp-5axis-xyzbc-trt-sim-plan/tools/trace-pause-position-json.mjs`
10. 真实页面验证输出:`pause_position_status=passed-position-frozen`trace 文件为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/pause-position-traces/pause-position-20260706T041750Z/trace.json`
11. 分析 trace暂停开始 `sampleIndex=39``currentVelocity=0``axisPose={x:15.29042230208562,y:15.194488529384802,z:11.633163978655007,a:0,b:10.3448,c:23.2759}`;暂停保持约 3.5 秒后仍为同一 `sampleIndex=39`、同一 axisPose、`currentVelocity=0``changedFields=[]``positionChangedAfterPause=false`
12. 运行 `git diff --check`,通过,无空白错误。
### 结论
确认暂停按钮“没有反应”的主要风险是条件门禁过度依赖 `taskMode=auto/mdi`,当 Web 状态中实际已经运行但 mode 仍显示/滞后为 manual 时,`PAUSE_RESUME` 会被旧条件拦截。已改为以实际运行/暂停状态为准,并在暂停/恢复时自动修正程序控制模式为 auto。构建、Node 回归和真实页面暂停 trace 均已通过。
## 2026-07-06 00:34 EDT
### 提问
用户要求完全对标 `/home/mes123456/cnc_wams/linuxcnc``xyzbc-trt` 的解除 `ESTOP`、上电、`Home All``Run`、暂停功能,修改 Web 仿真系统 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的混乱逻辑,并进行严格验证。用户同时要求新任务先写入 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working` 文档后再执行。
### 执行过程
1. 先在 `working` 目录新增任务文档:`working/13-20260706-xyzbc-trt-任务态按钮逻辑严格对标.md`,记录用户提问、执行目标、执行顺序和验证要求。
2. 读取 Web 当前实现:`app/src/state/store.js``app/src/state/linuxcnc-task-policy.js``app/src/ui/axis-shell.js``tests/node/verify_xyzbc_trt_web_app.mjs``tests/browser/xyzbc_trt_browser_smoke.html`
3. 读取 LinuxCNC 参考:`linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini`,确认 `DISPLAY=axis`、默认打开 `./demos/xyzbc_switchkins.ngc`、坐标为 `XYZBC`、5 个 joint 均可模拟回零,且未启用 `NO_FORCE_HOMING`
4. 读取 LinuxCNC AXIS 源码:`linuxcnc/src/emc/usr_intf/axis/scripts/axis.py``linuxcnc/share/axis/tcl/axis.tcl`。确认 `estop_clicked()` 只在 `STATE_ESTOP``STATE_ESTOP_RESET` 间切换;`onoff_clicked()` 只在 `STATE_ESTOP_RESET` 后上电,否则关机;`home_all_joints()` 独立执行;`task_run()` 通过 `ensure_mode(MODE_AUTO)` 后执行 `AUTO_RUN`,不会在 Run 内自动上电或自动 Home`task_pause()``task_resume()``task_pauseresume()` 分别对标暂停、恢复、暂停/恢复。
5. 定位 Web 混乱点:`RUN_READY` 原逻辑会一次性设置 `powerOn=true``allHomed=true``mode=auto`,并在 task/HAL 路径发送 `EMC_TASK_SET_STATE ON``EMC_JOINT_HOME -1``EMC_TASK_SET_MODE AUTO`,等于绕过了用户要求分开的“解除 ESTOP / 上电 / Home All”按钮顺序。
6. 定位第二个混乱点:`RUN_FROM_OPERATOR` 原逻辑在 task/HAL 路径也会强制写入 `powerOn=true``allHomed=true``mode=auto`,并在真正运行前再次发送 `EMC_JOINT_HOME -1`,导致点击 Run 可隐式替代 Home All。
7. 修改 `app/src/state/store.js``runReadySequence()` 现在只负责确保默认 G-code 已选择、task/HAL 会话可初始化,并给出提示;不再修改电源状态、不再回零、不再切 AUTO、不再切 TCP kins。
8. 修改 `app/src/state/store.js``operatorRunSequence()` 在执行前构造 AUTO 视角的 gate 校验;如果未上电则只提示 `run blocked: machine must be on`;如果未 Home 则只提示 `run blocked: home machine first`Run 仍保留 AXIS 的 `ensure_mode(MODE_AUTO)` 行为,即在已上电且已回零后可从 manual 入口切到 auto 后运行。
9. 修改 `app/src/state/store.js`task/HAL Run 路径不再写入假的 `powerOn/allHomed`,最终 `PLAN_RUN` 命令序列也删除了隐式 `EMC_JOINT_HOME -1`,只保留已满足条件后的 `EMC_TASK_SET_STATE ON``EMC_TASK_SET_MODE AUTO``EMC_TASK_PLAN_RUN`
10. 修改 `app/src/ui/axis-shell.js`:更新 `menu-run-ready` 的 parity 描述,明确其作用为打开/准备程序不替代上电、Home All 或 Run。
11. 修改 `tests/node/verify_xyzbc_trt_web_app.mjs`:增加断言验证 `RUN_READY` 不再改变 `powerOn/allHomed/mode`;验证未上电 Run 被阻止;验证上电后未回零 Run 被阻止;验证完成 `TOGGLE_POWER -> HOME -> RUN_FROM_OPERATOR` 后才运行。
12. 修改 `tests/browser/xyzbc_trt_browser_smoke.html`:在真实按钮流程中增加“解除急停后未上电点击 Run 被阻止”和“上电后未回零点击 Run 被阻止”的页面级断言;更新 `Run Ready` 菜单断言,确认它不再把 machine mode 改为 auto。
13. 执行 `node tests/node/verify_xyzbc_trt_web_app.mjs`,首次发现新增测试使用底层 `RUN` 动作时返回 `run blocked: switch to auto mode first`。分析后确认底层 `RUN` 是任务层命令,用户按钮路径应使用 `RUN_FROM_OPERATOR` 对标 AXIS 的 `task_run()`,因此调整测试入口为 `RUN_FROM_OPERATOR`
14. 重新执行 `node tests/node/verify_xyzbc_trt_web_app.mjs`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
15. 查看 `app/package.json`,确认项目没有 `npm test` 脚本,实际可用脚本为 `build``smoke:node``smoke:browser``evidence:web``evidence:compare`
16. 执行 `npm run build`,通过,输出 `gmoccapy_static_build=ok`,并同步更新 `app/dist`
17. 执行 `npm run smoke:node`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
18. 执行 `npm run smoke:browser`,通过,输出 `xyzbc_trt_browser_smoke=ok`真实浏览器按钮流覆盖解除急停、上电、Home All、Run、暂停/继续、步进、停止等路径。
19. 执行 `npm run evidence:web`,通过,生成 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json`
20. 执行 `npm run evidence:compare`,通过,输出 `compare_xyzbc_trt_status=pass`,生成 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/compare-xyzbc-trt-evidence.json`
21. 执行源码检查:确认 `operatorRunSequence()` 中不再包含 `EMC_JOINT_HOME`;剩余 `EMC_JOINT_HOME` 只存在于 `HOME` 行为和 task/HAL 会话恢复已保存 homed 状态的逻辑中。
22. 查看工作区状态发现存在若干本轮开始前已有的修改和未跟踪证据目录本次任务只基于当前工作区修改状态机、AXIS 元数据、节点/浏览器验证、dist 构建产物、新任务文档和 Web 证据。
### 结论
已完成对 `xyzbc-trt` 关键按钮顺序的严格对标Web 现在不会再用 `Run Ready``Run` 隐式代替“上电”和 `Home All`;用户必须按 LinuxCNC/AXIS 语义满足解除急停、上电、回零后Run 才能执行。Run 仍保留 AXIS 的自动切 AUTO 行为;暂停/继续路径继续通过浏览器烟测验证。构建、节点烟测、浏览器烟测、Web 证据采集和 native/Web 对比均通过。
## 2026-07-06 01:33 EDT
### 提问
用户要求严格对标“暂停”按钮的逻辑,指出暂停按钮一直不好用。
### 执行过程
1. 按用户要求先新增任务文档:`web-rtcp-5axis-xyzbc-trt-sim-plan/working/14-20260706-xyzbc-trt-暂停按钮严格对标.md`,记录本次目标、对标语义和验证要求。
2. 读取 LinuxCNC AXIS 源码:`linuxcnc/src/emc/usr_intf/axis/scripts/axis.py``task_pause()``task_resume()``task_pauseresume()`;读取 `linuxcnc/share/axis/tcl/axis.tcl`,确认工具栏双竖线按钮绑定 `task_pauseresume`
3. 确认 LinuxCNC 语义:工具栏暂停/继续按钮只在 task mode 为 AUTO/MDI 时响应;若 `s.paused` 则执行 `AUTO_RESUME`;若 interpreter 不是 idle 则执行 `AUTO_PAUSE`;菜单 Pause 对标 `task_pause`,菜单 Resume 对标 `task_resume`
4. 读取 Web 当前实现:`app/src/ui/axis-shell.js` 中工具栏 `tbtn_pause` 分发 `PAUSE_RESUME``app/src/state/store.js``PAUSE``PAUSE_RESUME``RESUME``app/src/state/linuxcnc-task-policy.js` 中暂停/恢复 gate。
5. 先运行真实页面暂停位置 trace`APP_URL_PATH=/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html node tools/trace-pause-position-json.mjs`。初始结果通过,输出 `pause_position_status=passed-position-frozen`trace 为 `working/pause-position-traces/pause-position-20260706T051917Z/trace.json`
6. 运行完整真实按钮 50ms 流程:`RUN_STABLE_BEFORE_FIRST_PAUSE_MS=3000 node tools/verify-estop-power-home-run-pause-50ms.mjs`。初始结果通过,输出 `verification_status=passed`,截图目录为 `working/screenshots/estop-power-home-run-pause-50ms-20260706T051953Z`
7. 虽然现有真实测试通过,但审查发现潜在不稳定点:`PAUSE` 会先本地置为 paused再异步发送 task/HAL `EMC_TASK_PLAN_PAUSE`;如果旧的 task/HAL 状态 tick 或命令回读随后到达,理论上可把 Web UI 从 paused 覆盖回 running用户会感知为“点击没有反应”或“一闪又继续”。
8. 修改 `app/src/state/store.js`:新增 `taskHalPauseLock` 状态,用于记录暂停按钮触发时的 sampleIndex、motionIndex、activeLine、axisPose、tcpPose 和来源。
9. 修改 `PAUSE` 分支:无论 task/HAL 路径还是 fixture 路径,暂停成功进入本地 paused 时都创建 `taskHalPauseLock`并将速度归零、runtime feedback 速度归零。
10. 修改 `RESUME` 分支:恢复时清除 `taskHalPauseLock`,再按 LinuxCNC `AUTO_RESUME` 语义恢复 running。
11. 修改 `RUN``RUN_FROM_OPERATOR``STEP``STOP/ABORT` 和 task/HAL 命令失败路径:这些路径会清除 `taskHalPauseLock`,避免暂停锁残留影响后续运行。
12. 修改 `applyTaskHalStatusPatch()`:如果 `taskHalPauseLock.active=true`,则暂停锁优先级高于异步 task/HAL 状态回写;即使收到旧的 `READING/running` 状态,也保持 `runState=paused``interpState=paused``taskPaused=true`、速度为 0并使用锁定的 sampleIndex、motionIndex 和 axisPose。
13. 修改 `tests/node/verify_xyzbc_trt_web_app.mjs`:新增回归测试,构造运行中点击 `PAUSE_RESUME` 后,再手动分发一个模拟的旧 task/HAL running 状态回写;断言 Web 必须仍为 paused、sampleIndex 不变、axisPose 不变、速度为 0再次 `PAUSE_RESUME` 后恢复 running并清除暂停锁。
14. 运行 `node tests/node/verify_xyzbc_trt_web_app.mjs`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
15. 运行 `npm run build`,通过,输出 `gmoccapy_static_build=ok`,同步更新 `app/dist/src/state/store.js`
16. 运行 `npm run smoke:node`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
17. 运行 `npm run smoke:browser`,通过,输出 `xyzbc_trt_browser_smoke=ok`
18. 再次运行真实页面暂停位置 trace`APP_URL_PATH=/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html node tools/trace-pause-position-json.mjs`,通过,输出 `pause_position_status=passed-position-frozen`trace 为 `working/pause-position-traces/pause-position-20260706T053021Z/trace.json``position_changed=false``changed_fields=`
19. 运行 `npm run evidence:web && npm run evidence:compare`,通过,生成/更新 `working/evidence/web-xyzbc-trt-evidence.json``working/evidence/compare-xyzbc-trt-evidence.json`,输出 `compare_xyzbc_trt_status=pass`
20. 再次运行完整真实按钮 50ms 流程:`RUN_STABLE_BEFORE_FIRST_PAUSE_MS=3000 node tools/verify-estop-power-home-run-pause-50ms.mjs`,通过,输出 `verification_status=passed`,截图目录为 `working/screenshots/estop-power-home-run-pause-50ms-20260706T053045Z`manifest 为 `working/screenshots/estop-power-home-run-pause-50ms-20260706T053045Z/manifest.json`,采集 420 帧。
21. 抽取 manifest 关键结果:第一次暂停保持 5 秒通过,状态为 `runState=paused``mode=auto``interpState=paused``taskPaused=true``sampleIndex=159``currentVelocity=0`、operatorMessage 为 `task/HAL program paused`;第二次暂停保持 5 秒通过,状态为 `sampleIndex=393``currentVelocity=0`,仍为 paused。
22. 运行 `git diff --check`,通过,无空白错误。
### 结论
已按 LinuxCNC AXIS 的 `task_pauseresume` 语义强化 Web 暂停按钮逻辑。关键修复是增加 `taskHalPauseLock`,让暂停按钮触发后的 paused 状态、位置、sampleIndex 和速度在恢复前不会被异步 task/HAL 旧状态覆盖。节点回归、构建、浏览器烟测、真实位置冻结 trace、完整 50ms 长流程和 Web/native 证据对比均已通过。
## 2026-07-06 01:35 EDT
### 提问
用户要求对标 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS主控制按钮功能先决条件与状态影响详解.md`,修改项目 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan`,并依据该项目 `working` 目录下文档实施。
### 执行过程
1. 读取参考文档 `doc/AXIS主控制按钮功能先决条件与状态影响详解.md`,确认 AXIS 主控制按钮链路和源码语义:急停/解除急停、上电、Home All、Run、Pause、Resume、Step 均通过 LinuxCNC task state/mode/interp state 门禁执行。
2. 查看 `working` 目录,重点读取 `12-20260704-真实执行复验与缺失功能工作计划.md``13-20260706-xyzbc-trt-任务态按钮逻辑严格对标.md``14-20260706-xyzbc-trt-暂停按钮严格对标.md`,确认前序任务已经要求严格对标 ESTOP、上电、Home、Run、Pause/Resume。
3. 检查工作区状态,发现本轮开始前已有多处未提交修改和新增证据文件,包括 `app/src/state/store.js``linuxcnc-task-policy.js``axis-shell.js`、测试文件、working 证据等;本轮未回退这些既有改动,只在当前状态基础上补齐。
4. 读取 `app/src/state/linuxcnc-task-policy.js`,发现 `canPause` 仍允许 `AUTO/MDI` 任一模式,不要求解释器处于 `READING/WAITING``PAUSE` gate 还允许通过 `runState=running/stepping` 绕过严格解释器状态。
5. 读取 `app/src/state/store.js`,定位 `PAUSE``PAUSE_RESUME``RESUME` 分支,发现工具栏 `PAUSE_RESUME` 仍会在 `runState=running/stepping` 时触发暂停,即使 task mode 残留为 manual 或 interpreter 已 idle。
6. 读取 `app/src/ui/axis-shell.js`,确认菜单 Pause 分发 `PAUSE`,菜单 Resume 分发 `RESUME`,工具栏双竖线按钮 `tbtn_pause` 分发 `PAUSE_RESUME`,与 AXIS 的菜单/工具栏语义可一一对应。
7. 读取本地 LinuxCNC 源码 `/home/mes123456/cnc_wams/linuxcnc/src/emc/usr_intf/axis/scripts/axis.py``task_pause()``task_resume()``task_pauseresume()` 原文,确认真实语义:菜单 Pause 必须 `MODE_AUTO``INTERP_READING/INTERP_WAITING`Resume 必须已 paused 且模式为 AUTO/MDI工具栏 pause/resume 必须先满足 AUTO/MDIpaused 时恢复,非 idle 时暂停。
8. 修改 `app/src/state/linuxcnc-task-policy.js`:将 `canPause` 收紧为 `STATE_ON + MODE_AUTO + INTERP_READING/WAITING`;菜单 `PAUSE` 不再使用 `runState` 作为替代门禁;工具栏 `pauseresume` 暂停分支必须为 AUTO/MDI 且 interpreter 非 idle。
9. 修改 `app/src/state/store.js``PAUSE_RESUME` 先规范化 task mode若不是 AUTO/MDI 直接返回 `pause blocked: task mode must be auto or MDI`;若处于 paused 则分发 `RESUME`;若 interpreter 非 idle 才分发 `PAUSE`;若 interpreter idle 则返回 `pause ignored: interpreter is idle`
10. 修改 `tests/node/verify_xyzbc_trt_web_app.mjs`把旧断言“runState=running 但 interpState=idle 仍可 Pause”改为必须阻止消息为 `pause blocked: interpreter is not running`把旧断言“manual 残留 running 可被 PAUSE_RESUME 强制改成 AUTO paused”改为必须阻止保持 manual/reading/running不创建 pause lock。
11. 运行 `npm --prefix app run smoke:node`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
12. 运行 `npm --prefix app run build`,通过,输出 `gmoccapy_static_build=ok`,将 `app/dist/src/...``src` 重新生成。
13. 运行 `npm --prefix app run smoke:browser`,通过,输出 `xyzbc_trt_browser_smoke=ok`,验证真实 DOM 按钮路径和浏览器运行态仍正常。
14. 运行 `npm --prefix app run evidence:web`,通过,重新生成 `working/evidence/web-xyzbc-trt-evidence.json`
15. 再次运行 `npm --prefix app run smoke:node`,通过,确认 evidence 后节点 smoke 仍稳定。
16. 运行 `npm --prefix app run evidence:compare`,通过,输出 `compare_xyzbc_trt_status=pass`,重新生成 `working/evidence/compare-xyzbc-trt-evidence.json`
17. 新增 `working/15-20260706-AXIS主控制按钮严格对标补齐.md`,记录本轮用户提问、目标、修改内容、验证命令和结果。
18. 查看 diff 与状态,确认本轮关键涉及 `app/src/state/linuxcnc-task-policy.js``app/src/state/store.js``tests/node/verify_xyzbc_trt_web_app.mjs`、构建同步的 `app/dist/src/...`、Web/compare evidence以及新增 working 文档;`tests/browser/xyzbc_trt_browser_smoke.html` 在本轮开始前已处于修改态,本轮未进一步修改该文件。
### 结论
已按 `AXIS主控制按钮功能先决条件与状态影响详解.md` 和 LinuxCNC AXIS 源码补齐暂停/恢复门禁。Web 仿真现在不再仅凭 `runState` 的残留值触发 Pause菜单 Pause 严格要求 AUTO 且 interpreter 为 READING/WAITING工具栏 pause/resume 严格要求 AUTO/MDI 且 interpreter 非 idlemanual 残留运行态不会被强制改成 AUTO paused。节点 smoke、构建、浏览器 smoke、Web evidence 和 native/Web compare 均通过。
## 2026-07-06 01:37 EDT
### 提问
用户要求“按新文档执行任务”。此处新文档为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/15-20260706-AXIS主控制按钮严格对标补齐.md`
### 执行过程
1. 读取 `working/15-20260706-AXIS主控制按钮严格对标补齐.md`,确认任务目标为复核 AXIS 主控制按钮状态门禁,重点是菜单 Pause、工具栏 pause/resume 在 idle、manual、AUTO/MDI、paused 状态下的严格行为,并重新执行构建、节点 smoke、浏览器 smoke、Web evidence 与 compare。
2. 查看工作区状态,确认仍存在本轮前序任务产生和之前已有的未提交修改、未跟踪文档、验证工具、截图和 evidence本次按文档继续执行不回退既有改动。
3. 使用 `rg` 检查关键源码和测试断言,确认 `app/src/state/linuxcnc-task-policy.js``canPause` 已为 `STATE_ON + MODE_AUTO + INTERP_READING/WAITING``app/src/state/store.js` 中存在 `PAUSE``PAUSE_RESUME` 分支,`tests/node/verify_xyzbc_trt_web_app.mjs` 中存在 idle interpreter 和 manual 残留状态的严格阻止断言。
4. 执行 `npm --prefix app run smoke:node`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
5. 执行 `npm --prefix app run build`,通过,输出 `gmoccapy_static_build=ok`,重新生成 `app/dist`
6. 执行 `npm --prefix app run smoke:browser`,通过,输出 `xyzbc_trt_browser_smoke=ok`,验证真实浏览器按钮路径仍正常。
7. 执行 `npm --prefix app run evidence:web`,通过,输出 `web_xyzbc_trt_evidence=/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json`
8. 再次执行 `npm --prefix app run smoke:node`,通过,确认 evidence 生成后节点 smoke 仍稳定。
9. 执行 `npm --prefix app run evidence:compare`,通过,输出 `compare_xyzbc_trt_status=pass`,生成 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/compare-xyzbc-trt-evidence.json`
10. 抽查 evidence JSON`web-xyzbc-trt-evidence.json` 状态为 `ready-for-wasm-runtime``blockers=[]``compare-xyzbc-trt-evidence.json` 状态为 `pass`summary 为 `checkCount=60``passCount=60``failCount=0``blockers=[]``nativeStatus=ok``webStatus=ready-for-wasm-runtime`
### 结论
已按新文档完成执行和复验。AXIS 主控制按钮严格对标补齐后的源码与测试仍在位;节点 smoke、构建、浏览器 smoke、Web evidence 和 native/Web compare 全部通过compare 为 60/60 pass无 blocker。
## 2026-07-06 01:49 EDT
### 提问
用户要求项目 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 完全对标 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS主控制按钮功能先决条件与状态影响详解.md`
### 执行过程
1. 重新读取 `doc/AXIS主控制按钮功能先决条件与状态影响详解.md` 全文,逐项确认 ESTOP、RESET、POWER、HOME、RUN、PAUSE、RESUME、PAUSE_RESUME、STEP 的 AXIS 回调、LinuxCNC API、NML 命令、先决条件和状态影响。
2. 查看当前工作区状态,确认已有多处未提交修改和未跟踪 evidence、working 文档、截图目录;本轮继续基于当前工作区补齐对标,不回退既有改动。
3. 读取 `app/src/state/linuxcnc-task-policy.js`,确认暂停门禁已按前序任务收紧为 AUTO 且解释器 READING/WAITING确认 RUN、HOME、MDI 的基本门禁已覆盖上电、回零和模式要求。
4. 读取 `app/src/state/store.js``TOGGLE_POWER``ESTOP``RESET``RUN``STOP/ABORT``PAUSE``PAUSE_RESUME``RESUME``STEP``HOME` 分支,逐项对照参考文档。
5. 读取 LinuxCNC AXIS 源码 `/home/mes123456/cnc_wams/linuxcnc/src/emc/usr_intf/axis/scripts/axis.py``estop_clicked()``onoff_clicked()`,确认电源按钮真实逻辑为:只有 `STATE_ESTOP_RESET` 时发送 `STATE_ON`,其他状态都发送 `STATE_OFF`
6. 读取 LinuxCNC task 源码 `/home/mes123456/cnc_wams/linuxcnc/src/emc/task/emctask.cc``emcTaskSetState(OFF)`,确认下电会 abort motion/spindle、disable traj、关闭冷却、abort task、`emcJointUnhome(-2)` 并同步解释器。
7. 发现 Web 原 `TOGGLE_POWER` 与 AXIS 不完全一致:策略层在 `STATE_ESTOP` 下阻止电源按钮,并提示先 reset状态层按 on/off 切换,未精确表达 `onoff_clicked()` 的 “ESTOP_RESET 才上电,否则 OFF” 语义。
8. 修改 `app/src/state/linuxcnc-task-policy.js`:移除 `TOGGLE_POWER` 在 ESTOP 状态的阻止,让按钮动作可以进入 OFF 分支,对标 AXIS 的 `else: c.state(STATE_OFF)`
9. 修改 `app/src/state/store.js``TOGGLE_POWER` 改为 `taskState === "estop-reset"` 时上电,否则下电到 `taskState="off"`task/HAL 路径上电发送 `EMC_TASK_SET_STATE ON`,否则发送 `EMC_TASK_SET_STATE OFF`
10.`app/src/state/store.js` 新增 `createPowerToggleMachinePatch()`,统一上电/下电后的 machine 状态;下电时设置 `powerOn=false``estopActive=false``taskState="off"`、解释器 idle、`taskPaused=false`,并清除 `allHomed=false`,对标 volatile home 清除效果。
11. 修改 `tests/node/verify_xyzbc_trt_web_app.mjs`:新增 ESTOP 下点击 Power 进入 OFF 的断言;新增 OFF 下继续点击 Power 仍保持 OFF不能绕过 reset 直接上电的断言;新增下电清除 `allHomed` 的断言。
12. 修改 `tests/node/verify_xyzbc_trt_web_app.mjs` 时,先因批量替换把同一文件中主按钮链路测试和后续 MDI/JOG/override 测试都改成同名 `controlStore`,随后复查变量作用域并修正:主按钮链路使用 `buttonStore`,后续控制面测试使用独立 `controlStore`,避免异步运行残留污染后续断言。
13. 修改 `tests/browser/xyzbc_trt_browser_smoke.html`:新增真实 DOM 路径验证ESTOP 后点击 Power 进入 OFFOFF 下继续点击 Power 保持 OFF再次 ESTOP/RESET 后才能上电。
14. 执行 `npm --prefix app run smoke:node`,首次失败在后续 MDI 测试 `M428` 期望 `tcp-xyzbc` 但实际仍为 `identity`。定位后确认原因是同一个 store 前面执行过运行/停止/下电链路,存在异步状态污染;改为使用独立 `controlStore` 后重新验证。
15. 重新执行 `npm --prefix app run smoke:node`,通过,输出 `xyzbc_trt_web_app_smoke=ok`
16. 执行 `npm --prefix app run build`,通过,输出 `gmoccapy_static_build=ok`,同步更新 `app/dist/src/...`
17. 执行 `npm --prefix app run smoke:browser`,通过,输出 `xyzbc_trt_browser_smoke=ok`,确认真实页面按钮路径接受新的 OFF 语义。
18. 执行 `npm --prefix app run evidence:web`,通过,重新生成 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json`
19. 再次执行 `npm --prefix app run smoke:node`,通过,确认 evidence 后节点 smoke 仍稳定。
20. 执行 `npm --prefix app run evidence:compare`,通过,输出 `compare_xyzbc_trt_status=pass`
21. 抽查 evidence JSON`web-xyzbc-trt-evidence.json` 状态为 `ready-for-wasm-runtime``blockers=[]``compare-xyzbc-trt-evidence.json` 状态为 `pass`summary 为 `checkCount=60``passCount=60``failCount=0``blockers=[]``nativeStatus=ok``webStatus=ready-for-wasm-runtime`
22. 新增 `working/16-20260706-AXIS主控制按钮完全对标复核.md`,记录本轮完全对标范围、发现的电源按钮偏差、修正内容和验证结果。
### 结论
已按参考文档完成主控制按钮全量复核,并修正 Web 电源按钮与 AXIS `onoff_clicked()` 不一致的问题。现在 Web 中 Power 只有在 `STATE_ESTOP_RESET` 时上电,其他状态下进入 `STATE_OFF`OFF 状态不能再次点击 Power 绕过 reset 直接上电;下电会清除 `allHomed` 并关闭运动相关状态。节点 smoke、构建、浏览器 smoke、Web evidence 和 native/Web compare 全部通过compare 为 60/60 pass无 blocker。
## 2026-07-06 02:35 EDT
### 提问
用户要求严格测试“暂停”按钮的功能。
### 执行过程
1. 读取 `web-rtcp-5axis-xyzbc-trt-sim-plan/working/14-20260706-xyzbc-trt-暂停按钮严格对标.md`,确认本次测试目标:工具栏双竖线按钮对标 AXIS `task_pauseresume`,菜单 Pause 对标 `task_pause`,菜单 Resume 对标 `task_resume`暂停后速度归零G-code 采样不继续推进DRO、axisPose、canvas toolhead 保持冻结。
2. 读取 `tools/trace-pause-position-json.mjs`,确认该工具会通过浏览器真实页面加载 `xyzbc_switchkins.ngc`,执行 ESTOP/RESET/POWER/HOME/RUN然后点击工具栏暂停按钮并连续采集暂停期间的 `runState``taskState``mode``interpState``taskPaused``sampleIndex``currentVelocity``axisPose``dro`、runtime 反馈、UI execution、canvas toolhead 和按钮状态。
3. 读取 `tools/verify-estop-power-home-run-pause-50ms.mjs`,确认该工具会用 Playwright 真实点击 AXIS DOM 控件,执行解除 ESTOP、上电、Home All、Run、第一次暂停保持 5 秒、第一次继续 10 秒、第二次暂停保持 5 秒、第二次继续 10 秒,并用 50ms 间隔截图采集全过程。
4. 查看工作区状态确认存在前序任务产生的未提交修改、working 文档、截图和 evidence本次只执行严格测试并新增测试报告不回退既有内容。
5. 执行 `npm --prefix app run build`,通过,输出 `gmoccapy_static_build=ok`
6. 执行 `npm --prefix app run smoke:node`,通过,输出 `xyzbc_trt_web_app_smoke=ok`,覆盖暂停/恢复节点回归断言。
7. 执行 `npm --prefix app run smoke:browser`,通过,输出 `xyzbc_trt_browser_smoke=ok`,覆盖真实 DOM 的暂停、恢复、菜单 Pause、菜单 Resume、Step 后恢复等按钮路径。
8. 执行 `APP_URL_PATH=/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html node tools/trace-pause-position-json.mjs`,通过,输出 `pause_position_status=passed-position-frozen`trace 文件为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/pause-position-traces/pause-position-20260706T063131Z/trace.json`
9. 分析 trace 结果:`position_changed=false``changed_fields=``pausedSampleCount=35`、baseline 为 `pause-start`;暂停期间 `runState=paused``taskState=on``mode=auto``interpState=paused``taskPaused=true``sampleIndex=48``currentVelocity=0`,且 `axisPose``dro`、runtime axis pose、runtime TCP、UI execution joint/TCP、canvas toolhead 均无变化;暂停按钮状态为 `paused=true`,标题为 `Resume program`
10. 执行 `RUN_STABLE_BEFORE_FIRST_PAUSE_MS=3000 node tools/verify-estop-power-home-run-pause-50ms.mjs`,等待长流程采集完成。该命令通过,输出 `verification_status=passed`,截图目录为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T063233Z`manifest 为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T063233Z/manifest.json`,共采集 `403` 帧,采样周期 `50ms`
11. 抽取 50ms manifest 关键断言:第一次暂停通过,状态为 `runState=paused``mode=auto``interpState=paused``taskPaused=true``sampleIndex=111``currentVelocity=0`;第一次暂停保持 5 秒仍为相同状态且 `sampleIndex=111`;第二次暂停通过,`sampleIndex=405``currentVelocity=0`;第二次暂停保持 5 秒仍为 `sampleIndex=405``currentVelocity=0`
12. 确认两次暂停都发生在真实展开 G-code `helix_bc.ngc` 第 17 行,语句为 `f#<frate> g2i#<r>z#<zmin> p#<n>`,即暂停发生在真实加工/圆弧进给段,不是空跑假路径。
13. 执行 `npm --prefix app run evidence:web`,通过,重新生成 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json`
14. 执行 `npm --prefix app run evidence:compare`,通过,输出 `compare_xyzbc_trt_status=pass`,确认本轮暂停测试后 Web/native evidence 仍通过。
15. 新增 `working/17-20260706-暂停按钮严格测试报告.md`,记录本次测试目标、命令、位置 trace、50ms 长流程截图验证、evidence compare 和结论。
### 结论
暂停按钮严格测试通过。节点回归、浏览器烟测、暂停位置 JSON trace、50ms 长流程截图验证和 Web/native evidence compare 均通过。暂停后 Web 状态稳定为 `paused`,速度为 0采样索引不推进DRO、axisPose、runtime 反馈、UI execution 和 canvas toolhead 均保持冻结;两次 5 秒暂停保持均通过。
## 2026-07-07 09:31 EDT
### 提问
用户要求根据 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS暂停按钮LinuxCNC调用链分析.md`,实现暂停功能,完善 `/home/mes123456/cnc_wams/wasm-port` 中 LinuxCNC 的 WASM 功能,最终实现 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的暂停按钮正确功能;并把完善方法和详细步骤写入 `/home/mes123456/cnc_wams/work/working8`,文档包括 01 项目功能内容、02 项目程序开发详细步骤、03 推进台账、04 任务矩阵、05 验收证据、06 决策记录,另加 README 索引。
### 执行过程
1. 读取项目根目录 `AGENTS.md`,确认本轮结束后必须把完整执行过程中文日志追加到 `gptlog-process/gpdlog.md`
2. 读取 `wasm-port/AGENTS.md``wasm-port/SKILL.md`,确认 `wasm-port` 的工作原则是复用 LinuxCNC 源码语义wrapper 只能做 standalone/WASM 边界适配,不能扩展成独立 CNC 语义实现;同时确认该子目录额外要求追加日志到 `/home/mes123456/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
3. 读取 `web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS暂停按钮LinuxCNC调用链分析.md`,确认 AXIS 暂停按钮调用链是 `task_pauseresume -> linuxcnc.command().auto(AUTO_PAUSE/AUTO_RESUME) -> EMC_TASK_PLAN_PAUSE/RESUME -> emcTrajPause/Resume -> EMCMOT_PAUSE/RESUME -> tpPause/tpResume`
4. 从调用链文档中提取关键结论:暂停不是只改 Web `runState`,而是必须同时维护 task/interpreter 暂停语义和 motion/trajectory 暂停语义task 层需要保存 `interpResumeState`、设置 `interpState=PAUSED``task_paused=1`motion 层需要设置 `motion.paused` 并阻止暂停期间继续推进普通运动队列TP 层后续应对标 `tpPause()/tpResume()`
5. 使用 `find``rg` 检查 `wasm-port``web-rtcp-5axis-xyzbc-trt-sim-plan` 文件结构,确认相关文件包括 `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp``wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c``wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c``wasm-port/runtime/sdk/src/linuxcnc-task-hal.js`、Web 侧 `app/src/state/store.js``app/src/state/linuxcnc-task-policy.js``app/src/ui/axis-shell.js`、测试 `verify_task_hal_wasm.mjs``verify_tp_wasm.mjs``verify_run_feedback_loop.mjs``trace-pause-position-json.mjs``verify-estop-power-home-run-pause-50ms.mjs`
6. 读取 `linuxcnc_task_hal_wasm.cpp` 关键段落,确认当前已经识别 `EMC_TASK_PLAN_PAUSE/STEP/RESUME`pause 会设置 `interp_state=PAUSED``exec_state=PAUSED``task_paused=true` 并转发 `EMC_TRAJ_PAUSE`resume 当前近似设置为 `READING`,尚未完整保存并输出 LinuxCNC 风格 `interpResumeState`
7. 读取 `linuxcnc_motion_runtime.c` 关键段落,确认当前已有 `paused` 字段、pause/resume/step 命令识别和状态 JSON 输出,但 pause 作为队列命令处理,暂停期间不消费普通 move 的逻辑仍偏简化STEP 也缺少 LinuxCNC `idForStep` 式自动回暂停语义。
8. 读取 `linuxcnc_tp_wasm.c``verify_tp_wasm.mjs`,确认 TP wrapper 已链接 vendored LinuxCNC TP API 并有基础队列/轨迹测试,但尚缺专门的 `tpPause()/tpResume()` 暂停恢复 probe。
9. 读取 Web 侧 `store.js``linuxcnc-task-policy.js` 暂停相关代码,确认前端已有 `PAUSE/RESUME/PAUSE_RESUME` 分支和 task policy gate但 resume 仍主要依赖 `interpState === paused`,需要接入 motion/traj paused 字段以贴近 AXIS `s.paused`
10. 读取既有 `working/14-20260706-xyzbc-trt-暂停按钮严格对标.md``working/17-20260706-暂停按钮严格测试报告.md`,确认前端暂停冻结历史测试曾通过,但该证据只能说明 Web 表层暂停冻结,不等于 `wasm-port` 底层 task/motion/TP 暂停链已经完整对标。
11. 检查 `work` 目录,确认 `work/working8` 已存在目录但尚无本轮要求文档。
12. 使用 `apply_patch` 新增 `work/working8/README.md`,写入本工作包索引、当前推进入口和当前结论,明确后续从任务矩阵 `W8-001` 开始推进。
13. 使用 `apply_patch` 新增 `work/working8/01-项目功能内容.md`写入项目目标、LinuxCNC 对标依据、当前代码基线、当前主要差距和完成定义。
14. 使用 `apply_patch` 新增 `work/working8/02-项目程序开发详细步骤.md`,按步骤写入 task 层 `interpResumeState` 补齐、motion immediate pause/resume、STEP 自动回暂停、Web task policy 接入 motion paused、TP pause/resume probe、构建回归和证据记录方法。
15. 使用 `apply_patch` 新增 `work/working8/03-推进台账.md`,记录 2026-07-07 轮次 1 的用户目标、本轮工作、改动文件、静态核对和下一步。
16. 使用 `apply_patch` 新增 `work/working8/04-任务矩阵.md`,建立 `W8-001``W8-014` 的任务编号、状态、目标文件和验收标准,明确前端 `runState=paused` 通过不等于底层 motion/STEP 任务完成。
17. 使用 `apply_patch` 新增 `work/working8/05-验收证据.md`,记录已有参考证据、本轮静态核对证据和待执行 WASM、Web Node、浏览器 trace、50ms 截图验收命令。
18. 使用 `apply_patch` 新增 `work/working8/06-决策记录.md`,记录暂停语义双层化、工具栏 toggle 与菜单命令分离、resume gate 优先 motion paused、immediate pause 不清空队列、STEP 不是普通 resume、短期修 wrapper 可观测语义长期迁移到 vendored TP 等决策。
19. 使用 `find work/working8` 校验生成文件,确认包含 README 和 01 至 06 六个要求文档。
20. 使用 `rg` 校验 `work/working8` 中关键内容,确认任务编号、`interpResumeState``motionPaused``tpPause``EMC_TASK_PLAN_PAUSE``EMCMOT_PAUSE``pause_position_status``verification_status` 等关键字段均可检索。
21. 使用 `git status --short -- work/working8 gptlog-process web-rtcp-5axis-sim-plan/gptlog-process` 查看本轮新增文档和日志状态,确认本轮未修改业务代码。
22. 未运行构建或自动化测试,因为本轮用户要求是把完善方法和详细步骤写入 `work/working8`,属于方案和推进文档建档。
### 结论
已完成 `/home/mes123456/cnc_wams/work/working8` 文档体系建设,新增 README 索引和 01 至 06 六个要求文档。文档明确了暂停按钮必须补齐 LinuxCNC task/interpreter 与 motion/trajectory 双层语义,拆分了 `W8-001``W8-014` 任务矩阵,并给出后续代码修改目标文件、验收标准和验证命令。本轮未修改业务代码,未运行测试。

View File

@@ -132,3 +132,32 @@
结论:
本轮已按 `working1` 完成并验证 G 代码执行相关闭环。working1 的 D1/D2/D3 关键验收点保持通过;同时修复了 G93 inverse-time G-code 执行计时中源程序 F 字丢失的问题。现在 Web 执行计时既保留 LinuxCNC canonical feed 输出,又能用源 G-code 的 `F159/F636` 正确计算每段真实执行时间,并能继续传递到 task/HAL motion plan 和运行反馈测试中。
## 2026-07-07 09:31 EDT
### 提问
用户要求根据 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS暂停按钮LinuxCNC调用链分析.md`,实现暂停功能,完善 `/home/mes123456/cnc_wams/wasm-port` 中 LinuxCNC 的 WASM 功能,最终实现 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的暂停按钮正确功能;并把完善方法和详细步骤写入 `/home/mes123456/cnc_wams/work/working8`,文档包括 01 项目功能内容、02 项目程序开发详细步骤、03 推进台账、04 任务矩阵、05 验收证据、06 决策记录,另加 README 索引。
### 执行过程
1. 读取项目根目录 `AGENTS.md`,确认本轮结束后必须把完整执行过程中文日志追加到 `gptlog-process/gpdlog.md`
2. 读取 `wasm-port/AGENTS.md``wasm-port/SKILL.md`,确认 `wasm-port` 的工作原则是复用 LinuxCNC 源码语义wrapper 只能做 standalone/WASM 边界适配,不能扩展成独立 CNC 语义实现;同时确认该子目录额外要求追加日志到 `/home/mes123456/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
3. 读取 `web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS暂停按钮LinuxCNC调用链分析.md`,确认 AXIS 暂停按钮调用链是 `task_pauseresume -> linuxcnc.command().auto(AUTO_PAUSE/AUTO_RESUME) -> EMC_TASK_PLAN_PAUSE/RESUME -> emcTrajPause/Resume -> EMCMOT_PAUSE/RESUME -> tpPause/tpResume`
4. 提取关键结论:暂停必须同时维护 task/interpreter 暂停语义和 motion/trajectory 暂停语义task 层需要保存 `interpResumeState`、设置 `interpState=PAUSED``task_paused=1`motion 层需要设置 `motion.paused` 并阻止暂停期间继续推进普通运动队列TP 层后续应对标 `tpPause()/tpResume()`
5. 检查 `wasm-port``web-rtcp-5axis-xyzbc-trt-sim-plan` 文件结构,定位 task-hal、motion runtime、TP wasm、SDK、Web store、task policy、UI、Node 测试和浏览器验证工具。
6. 读取 `linuxcnc_task_hal_wasm.cpp``linuxcnc_motion_runtime.c``linuxcnc_tp_wasm.c``verify_task_hal_wasm.mjs``verify_tp_wasm.mjs``store.js``linuxcnc-task-policy.js` 的关键暂停相关段落,确认当前已有最低限度 pause/resume 状态,但仍缺 `interpResumeState` 完整恢复、motion immediate pause、STEP 自动回暂停、TP pause/resume probe 和 Web motion paused gate。
7. 读取既有暂停对标和测试报告,确认前端冻结历史测试通过,但不能替代底层 WASM task/motion/TP 对标验收。
8. 新增 `work/working8/README.md`,写入索引、推进入口和当前结论。
9. 新增 `work/working8/01-项目功能内容.md`写入项目目标、LinuxCNC 对标依据、当前代码基线、当前差距和完成定义。
10. 新增 `work/working8/02-项目程序开发详细步骤.md`,写入 task、motion、STEP、Web policy、TP、构建回归和证据记录的详细推进步骤。
11. 新增 `work/working8/03-推进台账.md`,记录本轮做了什么、改了哪些文件、验证了什么和下一步。
12. 新增 `work/working8/04-任务矩阵.md`,建立 `W8-001``W8-014` 的任务编号、状态和验收标准。
13. 新增 `work/working8/05-验收证据.md`,记录已有参考证据、本轮静态核对证据和后续待跑命令。
14. 新增 `work/working8/06-决策记录.md`,记录暂停双层语义、工具栏 toggle 与菜单命令分离、resume gate 优先 motion paused、pause 不清空队列、STEP 不是普通 resume、短期修 wrapper 长期接入 vendored TP 等决策。
15. 校验 `work/working8` 文件结构和关键字段检索,确认 README 与 01 至 06 文档均存在且包含关键任务和验收内容。
16. 本轮未修改业务代码,未运行构建或自动化测试,因为用户本轮交付物是完善方法和详细步骤文档。
### 结论
已完成 `/home/mes123456/cnc_wams/work/working8` 文档体系建设,新增 README 索引和 01 至 06 六个要求文档。文档明确了暂停按钮必须补齐 LinuxCNC task/interpreter 与 motion/trajectory 双层语义,拆分了 `W8-001``W8-014` 任务矩阵,并给出后续代码修改目标文件、验收标准和验证命令。本轮未修改业务代码,未运行测试。

View File

@@ -79,6 +79,9 @@ export function buildVismachModelState(state = {}) {
}
function resolveAxisPose(state) {
if (state.runState === "paused" || state.machine?.interpState === "paused" || state.machine?.taskPaused === true) {
return state.axisPose || {};
}
if (state.programRuntimeFeedback?.axisPose) return state.programRuntimeFeedback.axisPose;
if (state.taskHalStatus?.ui?.axisPose) return state.taskHalStatus.ui.axisPose;
return state.axisPose || {};

View File

@@ -99,7 +99,7 @@ export function createLinuxCncTaskPolicyStatus(state) {
canRunAutoStrict: taskState === "on" && taskMode === "auto" && (allHomed || noForceHoming) &&
interpIdle && iniLoaded && machineFileStaged && machineFileOpened && taskHalRuntimeReady,
canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming),
canPause: taskState === "on" && (taskMode === "auto" || taskMode === "mdi"),
canPause: taskState === "on" && taskMode === "auto" && (interpState === "reading" || interpState === "waiting"),
canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && interpState === "paused",
canAbort: true,
canSpindle: taskState === "on",
@@ -116,9 +116,6 @@ export function gateLinuxCncTaskAction(state, action) {
switch (type) {
case "TOGGLE_POWER":
if (status.taskState === "estop") {
return block(status, "power on blocked: reset estop first");
}
return allow(status);
case "SET_MODE":
return gateMode(status, requestedMode);
@@ -164,10 +161,7 @@ export function gateLinuxCncTaskAction(state, action) {
case "PAUSE":
if (status.taskState !== "on") return block(status, "pause blocked: machine must be on");
{
const taskIsRunning = status.runState === "running" || status.runState === "stepping";
const interpIsRunning = status.interpState === "reading" || status.interpState === "waiting";
const interpCanPauseResume = status.interpState !== "idle" || taskIsRunning;
const pauseReady = action.source === "pauseresume" ? interpCanPauseResume : (interpIsRunning || taskIsRunning);
const requiredModeMessage = action.source === "pauseresume"
? "pause blocked: task mode must be auto or MDI"
: "pause blocked: task mode must be auto";
@@ -176,18 +170,18 @@ export function gateLinuxCncTaskAction(state, action) {
: "pause blocked: interpreter is not running";
if (action.source === "pauseresume") {
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
return block(status, requiredModeMessage);
}
if (status.taskMode !== "auto" && status.taskMode !== "mdi") return block(status, requiredModeMessage);
if (status.interpState === "idle") return block(status, notRunningMessage);
} else if (status.taskMode !== "auto") {
return block(status, requiredModeMessage);
}
if (!pauseReady) return block(status, notRunningMessage);
if (action.source !== "pauseresume" && status.taskMode !== "auto") return block(status, requiredModeMessage);
if (action.source !== "pauseresume" && !interpIsRunning) return block(status, notRunningMessage);
return allow(status);
}
case "RESUME":
if (status.taskState !== "on") return block(status, "resume blocked: machine must be on");
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
if (status.taskMode !== "auto" && status.taskMode !== "mdi" && status.runState !== "paused") {
return block(status, "resume blocked: task mode must be auto or MDI");
}
if (status.interpState !== "paused") return block(status, "resume blocked: interpreter is not paused");

View File

@@ -230,6 +230,7 @@ const initialState = {
taskHalExecutionPending: false,
taskHalExecutionSequence: 0,
taskHalStatusLoop: createTaskHalStatusLoopState(),
taskHalPauseLock: null,
taskHalFallbackReason: null,
pendingJogCommand: null,
interpreterExecutionPending: false,
@@ -1147,6 +1148,7 @@ export function createSimulationStore(seed = {}) {
setState({
taskHalFallbackReason: action.error,
taskHalExecutionPending: false,
taskHalPauseLock: null,
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
@@ -1269,50 +1271,34 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
const turningOn = state.machine.taskState === "estop-reset";
if (state.taskHalRuntime?.loaded) {
const nextMachine = createPowerToggleMachinePatch(state.machine, turningOn);
setState({
machine: {
...state.machine,
powerOn: !turningOff,
estopActive: false,
taskState: turningOff ? "estop-reset" : "on",
manualPanel: "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: turningOff ? "powered-off" : "idle",
kinsType: turningOff ? "identity" : state.kinsType,
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on",
machine: nextMachine,
runState: turningOn ? "idle" : "powered-off",
kinsType: turningOn ? state.kinsType : "identity",
rtcpState: turningOn ? state.rtcpState : "off",
feed: turningOn ? state.feed : { ...state.feed, currentVelocity: 0 },
coolant: turningOn ? state.coolant : { ...state.coolant, flood: false, mist: false },
spindle: turningOn ? state.spindle : stoppedSpindleState(state.spindle),
operatorMessage: turningOn ? "task/HAL machine power on" : "task/HAL machine power off",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: turningOff ? "ESTOP_RESET" : "ON" },
], { operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
{ type: "EMC_TASK_SET_STATE", state: turningOn ? "ON" : "OFF" },
], { operatorMessage: turningOn ? "task/HAL machine power on" : "task/HAL machine power off" }).catch(() => {});
break;
}
const nextMachine = createPowerToggleMachinePatch(state.machine, turningOn);
setState({
machine: {
...state.machine,
powerOn: !turningOff,
estopActive: false,
taskState: turningOff ? "estop-reset" : "on",
manualPanel: "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: turningOff ? "powered-off" : "idle",
kinsType: turningOff ? "identity" : state.kinsType,
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "machine power off" : "machine power on",
machine: nextMachine,
runState: turningOn ? "idle" : "powered-off",
kinsType: turningOn ? state.kinsType : "identity",
rtcpState: turningOn ? state.rtcpState : "off",
feed: turningOn ? state.feed : { ...state.feed, currentVelocity: 0 },
coolant: turningOn ? state.coolant : { ...state.coolant, flood: false, mist: false },
spindle: turningOn ? state.spindle : stoppedSpindleState(state.spindle),
operatorMessage: turningOn ? "machine power on" : "machine power off",
});
}
break;
@@ -1633,6 +1619,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: false,
},
runState: "running",
taskHalPauseLock: null,
operatorMessage: "task/HAL program run requested",
});
runValidatedTaskHalProgramRun().catch(() => {});
@@ -1658,6 +1645,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: false,
},
runState: playback.complete ? "complete" : "running",
taskHalPauseLock: null,
...playbackPatch,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, playback.runtimeFeedback, {
status: playback.complete ? "done" : "running",
@@ -1718,6 +1706,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: false,
},
runState: "stopped",
taskHalPauseLock: null,
feed: {
...state.feed,
currentVelocity: 0,
@@ -1766,19 +1755,24 @@ export function createSimulationStore(seed = {}) {
stopTaskHalStatusLoop("paused", { operatorMessage: "task/HAL pause requested" });
const pausedMachine = {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
};
const pauseLock = createTaskHalPauseLock(state, "task-hal-plan-pause");
setState({
machine: pausedMachine,
runState: "paused",
taskHalPauseLock: pauseLock,
feed: {
...state.feed,
currentVelocity: 0,
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "task/HAL pause requested",
});
runTaskHalCommandSequence([
@@ -1789,9 +1783,12 @@ export function createSimulationStore(seed = {}) {
}).catch(() => {});
break;
}
const pauseLock = createTaskHalPauseLock(state, "fixture-plan-pause");
setState({
machine: {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState,
@@ -1799,6 +1796,12 @@ export function createSimulationStore(seed = {}) {
taskPaused: true,
},
runState: "paused",
taskHalPauseLock: pauseLock,
feed: {
...state.feed,
currentVelocity: 0,
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "program paused",
});
}
@@ -1806,17 +1809,16 @@ export function createSimulationStore(seed = {}) {
case "PAUSE_RESUME":
{
const interpState = state.machine?.interpState || state.linuxCncTaskPolicy?.interpState || "idle";
const taskMode = state.machine?.mode || state.linuxCncTaskPolicy?.taskMode || "manual";
const taskIsRunning = state.runState === "running" || state.runState === "stepping";
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
if (taskMode === "auto" || taskMode === "mdi") {
dispatch({ type: "RESUME" });
break;
}
setState({ operatorMessage: "resume blocked: task mode must be auto or MDI" });
const taskMode = normalizeLinuxCncTaskMode(state.machine?.mode);
if (taskMode !== "auto" && taskMode !== "mdi") {
setState({ operatorMessage: "pause blocked: task mode must be auto or MDI" });
break;
}
if ((taskMode === "auto" || taskMode === "mdi") && (interpState !== "idle" || taskIsRunning)) {
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
dispatch({ type: "RESUME", source: "pauseresume" });
break;
}
if (interpState !== "idle") {
dispatch({ type: "PAUSE", source: "pauseresume" });
break;
}
@@ -1836,6 +1838,8 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpResumeState || "reading";
const resumedMachine = {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
@@ -1843,6 +1847,7 @@ export function createSimulationStore(seed = {}) {
setState({
machine: resumedMachine,
runState: resumeState === "reading" ? "running" : "idle",
taskHalPauseLock: null,
operatorMessage: "task/HAL resume requested",
});
runTaskHalCommandSequence([
@@ -1865,11 +1870,14 @@ export function createSimulationStore(seed = {}) {
setState({
machine: {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
},
runState: resumeState === "reading" ? "running" : "idle",
taskHalPauseLock: null,
operatorMessage: "program resumed",
});
}
@@ -1906,6 +1914,7 @@ export function createSimulationStore(seed = {}) {
setState({
machine: steppedMachine,
runState: "stepping",
taskHalPauseLock: null,
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
@@ -1946,6 +1955,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: true,
},
runState: "stepping",
taskHalPauseLock: null,
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
@@ -2602,49 +2612,17 @@ export function createSimulationStore(seed = {}) {
}
if (state.taskHalRuntime?.loaded) {
await initializeTaskHalSession({ openProgram: true });
await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_JOINT_HOME", joint: -1 },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
], {
taskCycles: 3,
operatorMessage: "RUN ready: power on, homed, auto mode",
allowFixtureSession: false,
preserveMachine: {
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
manualPanel: null,
allHomed: true,
},
setState({
operatorMessage: state.machine.powerOn && state.machine.allHomed
? "RUN ready: program opened; press Run"
: "RUN setup ready: reset ESTOP, power on, Home All, then Run",
});
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
if (tcpKinsType) {
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
}
return state;
}
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
setState({
machine: {
...state.machine,
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
manualPanel: null,
allHomed: true,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: "idle",
kinsType: tcpKinsType || "identity",
rtcpState: tcpKinsType ? "on" : "off",
operatorMessage: tcpKinsType
? "RUN ready: power on, homed, auto mode"
: "RUN ready: power on, homed, auto mode; TCP unavailable for this reference profile",
operatorMessage: state.machine.powerOn && state.machine.allHomed
? "RUN ready: program opened; press Run"
: "RUN setup ready: reset ESTOP, power on, Home All, then Run",
});
return state;
};
@@ -2657,6 +2635,19 @@ export function createSimulationStore(seed = {}) {
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
}
}
const runGateState = {
...state,
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
},
};
const runGate = gateLinuxCncTaskAction(runGateState, { type: "RUN" });
if (!runGate.allowed) {
setState({ operatorMessage: runGate.operatorMessage });
return state;
}
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
if (state.taskHalRuntime?.loaded) {
if (
@@ -2679,17 +2670,14 @@ export function createSimulationStore(seed = {}) {
setState({
machine: {
...state.machine,
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
manualPanel: null,
allHomed: true,
interpState: "reading",
interpResumeState: "reading",
taskPaused: false,
},
runState: "running",
taskHalPauseLock: null,
operatorMessage: "RUN ready: motion plan source ready; preparing task/HAL session",
});
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
@@ -2733,7 +2721,6 @@ export function createSimulationStore(seed = {}) {
});
const status = await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_JOINT_HOME", joint: -1 },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
], {
@@ -2755,8 +2742,15 @@ export function createSimulationStore(seed = {}) {
return state;
}
const policy = createLinuxCncTaskPolicyStatus(state);
if (policy.taskMode !== "auto" || !policy.allHomed || policy.taskState !== "on") {
await runReadySequence();
if (policy.taskMode !== "auto") {
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
},
operatorMessage: "mode auto",
});
}
dispatch({ type: "RUN" });
return state;
@@ -3778,27 +3772,38 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const pauseLock = state.taskHalPauseLock?.active === true ? state.taskHalPauseLock : null;
const rawTaskState = normalizeTaskHalTaskState(ui.taskState || task.state);
const taskState = preserveMachine?.powerOn && rawTaskState === "estop-reset"
? "on"
: rawTaskState;
const taskMode = normalizeLinuxCncTaskMode(preserveMachine?.mode || ui.taskMode || task.mode || state.machine.mode);
const taskMode = pauseLock
? programControlModeForMachine(state.machine)
: normalizeLinuxCncTaskMode(preserveMachine?.mode || ui.taskMode || task.mode || state.machine.mode);
const manualPanel = taskMode === "manual"
? (preserveMachine?.manualPanel || state.machine.manualPanel || "manual")
: null;
const interpState = Object.hasOwn(preserveMachine || {}, "interpState")
? normalizeTaskHalInterpState(preserveMachine.interpState)
: normalizeTaskHalInterpState(ui.interpState || task.interpState);
const interpState = pauseLock
? "paused"
: Object.hasOwn(preserveMachine || {}, "interpState")
? normalizeTaskHalInterpState(preserveMachine.interpState)
: normalizeTaskHalInterpState(ui.interpState || task.interpState);
const allHomed = Boolean(preserveMachine?.allHomed ?? state.machine.allHomed);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const axisPose = preserveAxisPose
? clampAxisPoseToProfile({ ...state.axisPose, ...preserveAxisPose }, state.profile)
: resolveTaskHalAxisPose(state, status);
const currentVelocity = Number.isFinite(ui.currentVelocity)
? Math.max(ui.currentVelocity, 0)
: state.feed.currentVelocity;
const paused = interpState === "paused" || motion.paused === true;
const paused = Boolean(pauseLock) || interpState === "paused" || motion.paused === true;
const axisPose = pauseLock?.axisPose
? clampAxisPoseToProfile(pauseLock.axisPose, state.profile)
: paused
? clampAxisPoseToProfile(state.axisPose, state.profile)
: preserveAxisPose
? clampAxisPoseToProfile({ ...state.axisPose, ...preserveAxisPose }, state.profile)
: resolveTaskHalAxisPose(state, status);
const currentVelocity = paused
? 0
: Number.isFinite(ui.currentVelocity)
? Math.max(ui.currentVelocity, 0)
: state.feed.currentVelocity;
const aborted = motion.aborted === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= openedProgramLineCount;
@@ -3819,7 +3824,13 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const shouldTrackProgramPlayback = runState === "running"
|| runState === "mdi"
|| (runState === "complete" && (state.runState === "running" || state.runState === "mdi" || state.runState === "complete"));
const taskHalSampleIndex = shouldTrackProgramPlayback
const taskHalSampleIndex = pauseLock
? clampNumber(
pauseLock.sampleIndex,
0,
Math.max(Number(state.programAxisPreviewPath?.samples?.length || state.programAxisPreviewPath?.sampleCount || 1) - 1, 0),
)
: shouldTrackProgramPlayback
? resolveRuntimeSampleIndexForTaskHalPose(state, axisPose, activeLine, runtimeFeedback.sampleIndex)
: clampNumber(
state.programExecutionSampleIndex || 0,
@@ -3829,7 +3840,17 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const idleRuntimeFeedback = {
...runtimeFeedback,
sampleIndex: taskHalSampleIndex,
motionIndex: Number(state.programExecutionMotionIndex || runtimeFeedback.motionIndex || 0),
motionIndex: pauseLock
? Number(pauseLock.motionIndex || 0)
: Number(state.programExecutionMotionIndex || runtimeFeedback.motionIndex || 0),
...(paused ? {
axisPose,
tcp: {
x: Number(state.tcpPose?.x ?? axisPose.x ?? 0),
y: Number(state.tcpPose?.y ?? axisPose.y ?? 0),
z: Number(state.tcpPose?.z ?? axisPose.z ?? 0),
},
} : {}),
};
const taskHalPlaybackPatch = shouldTrackProgramPlayback
? applyProgramPlaybackUiPatch(state, {
@@ -3837,11 +3858,15 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
axisPose,
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
motionIndex: runtimeFeedback.motionIndex,
motionIndex: pauseLock ? Number(pauseLock.motionIndex || 0) : runtimeFeedback.motionIndex,
sampleIndex: taskHalSampleIndex,
runtimeFeedback: {
...runtimeFeedback,
sampleIndex: taskHalSampleIndex,
motionIndex: pauseLock ? Number(pauseLock.motionIndex || 0) : runtimeFeedback.motionIndex,
axisPose,
currentVelocityMmPerMin: paused ? 0 : runtimeFeedback.currentVelocityMmPerMin,
requestedVelocityMmPerMin: paused ? 0 : runtimeFeedback.requestedVelocityMmPerMin,
},
preferRuntimeAxisPose: true,
})
@@ -3879,6 +3904,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
return {
taskHalStatus: status,
taskHalExecutionPending: false,
taskHalPauseLock: pauseLock ? { ...pauseLock, lastStatusAt: new Date().toISOString() } : state.taskHalPauseLock,
taskHalFallbackReason: null,
pendingJogCommand: null,
...taskHalPlaybackPatch,
@@ -3939,6 +3965,57 @@ function isTaskHalRunPausedByOperator(state = {}) {
|| state.machine?.taskPaused === true;
}
function programControlModeForMachine(machine = {}) {
return machine.mode === "mdi" ? "mdi" : "auto";
}
function createPowerToggleMachinePatch(machine = {}, turningOn = false) {
return {
...machine,
powerOn: turningOn,
estopActive: false,
taskState: turningOn ? "on" : "off",
manualPanel: "manual",
allHomed: turningOn ? Boolean(machine.allHomed) : false,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
};
}
function createTaskHalPauseLock(state = {}, source = "pause") {
return {
apiName: "web-rtcp-5axis-task-hal-pause-lock",
active: true,
source,
createdAt: new Date().toISOString(),
runState: "paused",
interpState: "paused",
sampleIndex: Number(state.programExecutionSampleIndex || 0),
motionIndex: Number(state.programExecutionMotionIndex || 0),
activeLine: Number(state.activeLine || state.programStartLine || 1),
axisPose: clampAxisPoseToProfile(state.axisPose || initialAxisPose, state.profile),
tcpPose: {
x: Number(state.tcpPose?.x || 0),
y: Number(state.tcpPose?.y || 0),
z: Number(state.tcpPose?.z || 0),
},
semanticBoundary: "linuxcnc_axis_task_pauseresume_freezes_task_hal_status_until_resume",
};
}
function zeroProgramRuntimeVelocity(feedback) {
if (!feedback) return feedback;
return {
...feedback,
currentVelocityMmPerMin: 0,
requestedVelocityMmPerMin: 0,
distanceToGo: 0,
dtg: feedback.dtg ? { ...feedback.dtg, x: 0, y: 0, z: 0 } : feedback.dtg,
activeDepth: 0,
};
}
function createStoppedProgramStatePatch(state, {
reason = "stopped",
operatorMessage = "program stopped",
@@ -3951,6 +4028,7 @@ function createStoppedProgramStatePatch(state, {
taskPaused: false,
},
runState: reason === "aborted" ? "stopped" : reason,
taskHalPauseLock: null,
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
@@ -4058,6 +4136,10 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const motion = status?.motionStatus?.motion || {};
const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0);
const motionProgramLine = Number(motion.programLine || 0);
const interpState = normalizeTaskHalInterpState(ui.interpState || status?.task?.interpState);
const paused = interpState === "paused" || motion.paused === true;
const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0);
const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60;
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "linuxcnc-task-motion-hal-wasm",
@@ -4072,8 +4154,8 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
timeSeconds: Number(ui.taskCycle || 0) * 0.01,
axisPose,
currentVelocityMmPerMin: Number(ui.currentVelocity || 0),
requestedVelocityMmPerMin: Number(motion.requestedVel || 0) * 60,
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: requestedVelocity,
distanceToGo: motion.inPosition === true ? 0 : 1,
dtg: { x: 0, y: 0, z: 0 },
queueDepth: Number(ui.motionQueueDepth || status?.motionStatus?.commandQueueDepth || 0),

View File

@@ -10,7 +10,7 @@ export const AXIS_BUTTON_PARITY = [
{ id: "menu-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" },
{ id: "menu-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine ON/OFF after estop reset" },
{ id: "menu-home-all", action: "home-all", sourceSymbol: "commands.home_all_joints", sourceLines: "axis.py:2586-2596", expected: "home all joints in manual mode" },
{ id: "menu-run-ready", action: "run-ready", sourceSymbol: "AXIS run preconditions", sourceLines: "axis.py:2308-2320", expected: "power on, home, auto mode and TCP kins ready" },
{ id: "menu-run-ready", action: "run-ready", sourceSymbol: "AXIS run preconditions", sourceLines: "axis.py:2379-2391", expected: "open/stage the selected program without substituting power, Home All, or Run" },
{ id: "menu-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "AUTO_RUN from current start line" },
{ id: "menu-pause", action: "pause", sourceSymbol: "commands.task_pause", sourceLines: "axis.py:2329-2333", expected: "AUTO_PAUSE when running" },
{ id: "menu-resume", action: "resume", sourceSymbol: "commands.task_resume", sourceLines: "axis.py:2344-2351", expected: "AUTO_RESUME when paused" },

View File

@@ -79,6 +79,9 @@ export function buildVismachModelState(state = {}) {
}
function resolveAxisPose(state) {
if (state.runState === "paused" || state.machine?.interpState === "paused" || state.machine?.taskPaused === true) {
return state.axisPose || {};
}
if (state.programRuntimeFeedback?.axisPose) return state.programRuntimeFeedback.axisPose;
if (state.taskHalStatus?.ui?.axisPose) return state.taskHalStatus.ui.axisPose;
return state.axisPose || {};

View File

@@ -99,7 +99,7 @@ export function createLinuxCncTaskPolicyStatus(state) {
canRunAutoStrict: taskState === "on" && taskMode === "auto" && (allHomed || noForceHoming) &&
interpIdle && iniLoaded && machineFileStaged && machineFileOpened && taskHalRuntimeReady,
canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming),
canPause: taskState === "on" && (taskMode === "auto" || taskMode === "mdi"),
canPause: taskState === "on" && taskMode === "auto" && (interpState === "reading" || interpState === "waiting"),
canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && interpState === "paused",
canAbort: true,
canSpindle: taskState === "on",
@@ -116,9 +116,6 @@ export function gateLinuxCncTaskAction(state, action) {
switch (type) {
case "TOGGLE_POWER":
if (status.taskState === "estop") {
return block(status, "power on blocked: reset estop first");
}
return allow(status);
case "SET_MODE":
return gateMode(status, requestedMode);
@@ -164,10 +161,7 @@ export function gateLinuxCncTaskAction(state, action) {
case "PAUSE":
if (status.taskState !== "on") return block(status, "pause blocked: machine must be on");
{
const taskIsRunning = status.runState === "running" || status.runState === "stepping";
const interpIsRunning = status.interpState === "reading" || status.interpState === "waiting";
const interpCanPauseResume = status.interpState !== "idle" || taskIsRunning;
const pauseReady = action.source === "pauseresume" ? interpCanPauseResume : (interpIsRunning || taskIsRunning);
const requiredModeMessage = action.source === "pauseresume"
? "pause blocked: task mode must be auto or MDI"
: "pause blocked: task mode must be auto";
@@ -176,18 +170,18 @@ export function gateLinuxCncTaskAction(state, action) {
: "pause blocked: interpreter is not running";
if (action.source === "pauseresume") {
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
return block(status, requiredModeMessage);
}
if (status.taskMode !== "auto" && status.taskMode !== "mdi") return block(status, requiredModeMessage);
if (status.interpState === "idle") return block(status, notRunningMessage);
} else if (status.taskMode !== "auto") {
return block(status, requiredModeMessage);
}
if (!pauseReady) return block(status, notRunningMessage);
if (action.source !== "pauseresume" && status.taskMode !== "auto") return block(status, requiredModeMessage);
if (action.source !== "pauseresume" && !interpIsRunning) return block(status, notRunningMessage);
return allow(status);
}
case "RESUME":
if (status.taskState !== "on") return block(status, "resume blocked: machine must be on");
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
if (status.taskMode !== "auto" && status.taskMode !== "mdi" && status.runState !== "paused") {
return block(status, "resume blocked: task mode must be auto or MDI");
}
if (status.interpState !== "paused") return block(status, "resume blocked: interpreter is not paused");

View File

@@ -230,6 +230,7 @@ const initialState = {
taskHalExecutionPending: false,
taskHalExecutionSequence: 0,
taskHalStatusLoop: createTaskHalStatusLoopState(),
taskHalPauseLock: null,
taskHalFallbackReason: null,
pendingJogCommand: null,
interpreterExecutionPending: false,
@@ -1147,6 +1148,7 @@ export function createSimulationStore(seed = {}) {
setState({
taskHalFallbackReason: action.error,
taskHalExecutionPending: false,
taskHalPauseLock: null,
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
@@ -1269,50 +1271,34 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
const turningOn = state.machine.taskState === "estop-reset";
if (state.taskHalRuntime?.loaded) {
const nextMachine = createPowerToggleMachinePatch(state.machine, turningOn);
setState({
machine: {
...state.machine,
powerOn: !turningOff,
estopActive: false,
taskState: turningOff ? "estop-reset" : "on",
manualPanel: "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: turningOff ? "powered-off" : "idle",
kinsType: turningOff ? "identity" : state.kinsType,
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on",
machine: nextMachine,
runState: turningOn ? "idle" : "powered-off",
kinsType: turningOn ? state.kinsType : "identity",
rtcpState: turningOn ? state.rtcpState : "off",
feed: turningOn ? state.feed : { ...state.feed, currentVelocity: 0 },
coolant: turningOn ? state.coolant : { ...state.coolant, flood: false, mist: false },
spindle: turningOn ? state.spindle : stoppedSpindleState(state.spindle),
operatorMessage: turningOn ? "task/HAL machine power on" : "task/HAL machine power off",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: turningOff ? "ESTOP_RESET" : "ON" },
], { operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
{ type: "EMC_TASK_SET_STATE", state: turningOn ? "ON" : "OFF" },
], { operatorMessage: turningOn ? "task/HAL machine power on" : "task/HAL machine power off" }).catch(() => {});
break;
}
const nextMachine = createPowerToggleMachinePatch(state.machine, turningOn);
setState({
machine: {
...state.machine,
powerOn: !turningOff,
estopActive: false,
taskState: turningOff ? "estop-reset" : "on",
manualPanel: "manual",
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: turningOff ? "powered-off" : "idle",
kinsType: turningOff ? "identity" : state.kinsType,
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "machine power off" : "machine power on",
machine: nextMachine,
runState: turningOn ? "idle" : "powered-off",
kinsType: turningOn ? state.kinsType : "identity",
rtcpState: turningOn ? state.rtcpState : "off",
feed: turningOn ? state.feed : { ...state.feed, currentVelocity: 0 },
coolant: turningOn ? state.coolant : { ...state.coolant, flood: false, mist: false },
spindle: turningOn ? state.spindle : stoppedSpindleState(state.spindle),
operatorMessage: turningOn ? "machine power on" : "machine power off",
});
}
break;
@@ -1633,6 +1619,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: false,
},
runState: "running",
taskHalPauseLock: null,
operatorMessage: "task/HAL program run requested",
});
runValidatedTaskHalProgramRun().catch(() => {});
@@ -1658,6 +1645,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: false,
},
runState: playback.complete ? "complete" : "running",
taskHalPauseLock: null,
...playbackPatch,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, playback.runtimeFeedback, {
status: playback.complete ? "done" : "running",
@@ -1718,6 +1706,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: false,
},
runState: "stopped",
taskHalPauseLock: null,
feed: {
...state.feed,
currentVelocity: 0,
@@ -1766,19 +1755,24 @@ export function createSimulationStore(seed = {}) {
stopTaskHalStatusLoop("paused", { operatorMessage: "task/HAL pause requested" });
const pausedMachine = {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
};
const pauseLock = createTaskHalPauseLock(state, "task-hal-plan-pause");
setState({
machine: pausedMachine,
runState: "paused",
taskHalPauseLock: pauseLock,
feed: {
...state.feed,
currentVelocity: 0,
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "task/HAL pause requested",
});
runTaskHalCommandSequence([
@@ -1789,9 +1783,12 @@ export function createSimulationStore(seed = {}) {
}).catch(() => {});
break;
}
const pauseLock = createTaskHalPauseLock(state, "fixture-plan-pause");
setState({
machine: {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState,
@@ -1799,6 +1796,12 @@ export function createSimulationStore(seed = {}) {
taskPaused: true,
},
runState: "paused",
taskHalPauseLock: pauseLock,
feed: {
...state.feed,
currentVelocity: 0,
},
programRuntimeFeedback: zeroProgramRuntimeVelocity(state.programRuntimeFeedback),
operatorMessage: "program paused",
});
}
@@ -1806,17 +1809,16 @@ export function createSimulationStore(seed = {}) {
case "PAUSE_RESUME":
{
const interpState = state.machine?.interpState || state.linuxCncTaskPolicy?.interpState || "idle";
const taskMode = state.machine?.mode || state.linuxCncTaskPolicy?.taskMode || "manual";
const taskIsRunning = state.runState === "running" || state.runState === "stepping";
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
if (taskMode === "auto" || taskMode === "mdi") {
dispatch({ type: "RESUME" });
break;
}
setState({ operatorMessage: "resume blocked: task mode must be auto or MDI" });
const taskMode = normalizeLinuxCncTaskMode(state.machine?.mode);
if (taskMode !== "auto" && taskMode !== "mdi") {
setState({ operatorMessage: "pause blocked: task mode must be auto or MDI" });
break;
}
if ((taskMode === "auto" || taskMode === "mdi") && (interpState !== "idle" || taskIsRunning)) {
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
dispatch({ type: "RESUME", source: "pauseresume" });
break;
}
if (interpState !== "idle") {
dispatch({ type: "PAUSE", source: "pauseresume" });
break;
}
@@ -1836,6 +1838,8 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpResumeState || "reading";
const resumedMachine = {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
@@ -1843,6 +1847,7 @@ export function createSimulationStore(seed = {}) {
setState({
machine: resumedMachine,
runState: resumeState === "reading" ? "running" : "idle",
taskHalPauseLock: null,
operatorMessage: "task/HAL resume requested",
});
runTaskHalCommandSequence([
@@ -1865,11 +1870,14 @@ export function createSimulationStore(seed = {}) {
setState({
machine: {
...state.machine,
mode: programControlModeForMachine(state.machine),
manualPanel: null,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
},
runState: resumeState === "reading" ? "running" : "idle",
taskHalPauseLock: null,
operatorMessage: "program resumed",
});
}
@@ -1906,6 +1914,7 @@ export function createSimulationStore(seed = {}) {
setState({
machine: steppedMachine,
runState: "stepping",
taskHalPauseLock: null,
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
@@ -1946,6 +1955,7 @@ export function createSimulationStore(seed = {}) {
taskPaused: true,
},
runState: "stepping",
taskHalPauseLock: null,
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
@@ -2602,49 +2612,17 @@ export function createSimulationStore(seed = {}) {
}
if (state.taskHalRuntime?.loaded) {
await initializeTaskHalSession({ openProgram: true });
await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_JOINT_HOME", joint: -1 },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
], {
taskCycles: 3,
operatorMessage: "RUN ready: power on, homed, auto mode",
allowFixtureSession: false,
preserveMachine: {
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
manualPanel: null,
allHomed: true,
},
setState({
operatorMessage: state.machine.powerOn && state.machine.allHomed
? "RUN ready: program opened; press Run"
: "RUN setup ready: reset ESTOP, power on, Home All, then Run",
});
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
if (tcpKinsType) {
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
}
return state;
}
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
setState({
machine: {
...state.machine,
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
manualPanel: null,
allHomed: true,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: "idle",
kinsType: tcpKinsType || "identity",
rtcpState: tcpKinsType ? "on" : "off",
operatorMessage: tcpKinsType
? "RUN ready: power on, homed, auto mode"
: "RUN ready: power on, homed, auto mode; TCP unavailable for this reference profile",
operatorMessage: state.machine.powerOn && state.machine.allHomed
? "RUN ready: program opened; press Run"
: "RUN setup ready: reset ESTOP, power on, Home All, then Run",
});
return state;
};
@@ -2657,6 +2635,19 @@ export function createSimulationStore(seed = {}) {
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
}
}
const runGateState = {
...state,
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
},
};
const runGate = gateLinuxCncTaskAction(runGateState, { type: "RUN" });
if (!runGate.allowed) {
setState({ operatorMessage: runGate.operatorMessage });
return state;
}
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
if (state.taskHalRuntime?.loaded) {
if (
@@ -2679,17 +2670,14 @@ export function createSimulationStore(seed = {}) {
setState({
machine: {
...state.machine,
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
manualPanel: null,
allHomed: true,
interpState: "reading",
interpResumeState: "reading",
taskPaused: false,
},
runState: "running",
taskHalPauseLock: null,
operatorMessage: "RUN ready: motion plan source ready; preparing task/HAL session",
});
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
@@ -2733,7 +2721,6 @@ export function createSimulationStore(seed = {}) {
});
const status = await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_JOINT_HOME", joint: -1 },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
], {
@@ -2755,8 +2742,15 @@ export function createSimulationStore(seed = {}) {
return state;
}
const policy = createLinuxCncTaskPolicyStatus(state);
if (policy.taskMode !== "auto" || !policy.allHomed || policy.taskState !== "on") {
await runReadySequence();
if (policy.taskMode !== "auto") {
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
},
operatorMessage: "mode auto",
});
}
dispatch({ type: "RUN" });
return state;
@@ -3778,27 +3772,38 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const pauseLock = state.taskHalPauseLock?.active === true ? state.taskHalPauseLock : null;
const rawTaskState = normalizeTaskHalTaskState(ui.taskState || task.state);
const taskState = preserveMachine?.powerOn && rawTaskState === "estop-reset"
? "on"
: rawTaskState;
const taskMode = normalizeLinuxCncTaskMode(preserveMachine?.mode || ui.taskMode || task.mode || state.machine.mode);
const taskMode = pauseLock
? programControlModeForMachine(state.machine)
: normalizeLinuxCncTaskMode(preserveMachine?.mode || ui.taskMode || task.mode || state.machine.mode);
const manualPanel = taskMode === "manual"
? (preserveMachine?.manualPanel || state.machine.manualPanel || "manual")
: null;
const interpState = Object.hasOwn(preserveMachine || {}, "interpState")
? normalizeTaskHalInterpState(preserveMachine.interpState)
: normalizeTaskHalInterpState(ui.interpState || task.interpState);
const interpState = pauseLock
? "paused"
: Object.hasOwn(preserveMachine || {}, "interpState")
? normalizeTaskHalInterpState(preserveMachine.interpState)
: normalizeTaskHalInterpState(ui.interpState || task.interpState);
const allHomed = Boolean(preserveMachine?.allHomed ?? state.machine.allHomed);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const axisPose = preserveAxisPose
? clampAxisPoseToProfile({ ...state.axisPose, ...preserveAxisPose }, state.profile)
: resolveTaskHalAxisPose(state, status);
const currentVelocity = Number.isFinite(ui.currentVelocity)
? Math.max(ui.currentVelocity, 0)
: state.feed.currentVelocity;
const paused = interpState === "paused" || motion.paused === true;
const paused = Boolean(pauseLock) || interpState === "paused" || motion.paused === true;
const axisPose = pauseLock?.axisPose
? clampAxisPoseToProfile(pauseLock.axisPose, state.profile)
: paused
? clampAxisPoseToProfile(state.axisPose, state.profile)
: preserveAxisPose
? clampAxisPoseToProfile({ ...state.axisPose, ...preserveAxisPose }, state.profile)
: resolveTaskHalAxisPose(state, status);
const currentVelocity = paused
? 0
: Number.isFinite(ui.currentVelocity)
? Math.max(ui.currentVelocity, 0)
: state.feed.currentVelocity;
const aborted = motion.aborted === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= openedProgramLineCount;
@@ -3819,7 +3824,13 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const shouldTrackProgramPlayback = runState === "running"
|| runState === "mdi"
|| (runState === "complete" && (state.runState === "running" || state.runState === "mdi" || state.runState === "complete"));
const taskHalSampleIndex = shouldTrackProgramPlayback
const taskHalSampleIndex = pauseLock
? clampNumber(
pauseLock.sampleIndex,
0,
Math.max(Number(state.programAxisPreviewPath?.samples?.length || state.programAxisPreviewPath?.sampleCount || 1) - 1, 0),
)
: shouldTrackProgramPlayback
? resolveRuntimeSampleIndexForTaskHalPose(state, axisPose, activeLine, runtimeFeedback.sampleIndex)
: clampNumber(
state.programExecutionSampleIndex || 0,
@@ -3829,7 +3840,17 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const idleRuntimeFeedback = {
...runtimeFeedback,
sampleIndex: taskHalSampleIndex,
motionIndex: Number(state.programExecutionMotionIndex || runtimeFeedback.motionIndex || 0),
motionIndex: pauseLock
? Number(pauseLock.motionIndex || 0)
: Number(state.programExecutionMotionIndex || runtimeFeedback.motionIndex || 0),
...(paused ? {
axisPose,
tcp: {
x: Number(state.tcpPose?.x ?? axisPose.x ?? 0),
y: Number(state.tcpPose?.y ?? axisPose.y ?? 0),
z: Number(state.tcpPose?.z ?? axisPose.z ?? 0),
},
} : {}),
};
const taskHalPlaybackPatch = shouldTrackProgramPlayback
? applyProgramPlaybackUiPatch(state, {
@@ -3837,11 +3858,15 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
axisPose,
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
motionIndex: runtimeFeedback.motionIndex,
motionIndex: pauseLock ? Number(pauseLock.motionIndex || 0) : runtimeFeedback.motionIndex,
sampleIndex: taskHalSampleIndex,
runtimeFeedback: {
...runtimeFeedback,
sampleIndex: taskHalSampleIndex,
motionIndex: pauseLock ? Number(pauseLock.motionIndex || 0) : runtimeFeedback.motionIndex,
axisPose,
currentVelocityMmPerMin: paused ? 0 : runtimeFeedback.currentVelocityMmPerMin,
requestedVelocityMmPerMin: paused ? 0 : runtimeFeedback.requestedVelocityMmPerMin,
},
preferRuntimeAxisPose: true,
})
@@ -3879,6 +3904,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
return {
taskHalStatus: status,
taskHalExecutionPending: false,
taskHalPauseLock: pauseLock ? { ...pauseLock, lastStatusAt: new Date().toISOString() } : state.taskHalPauseLock,
taskHalFallbackReason: null,
pendingJogCommand: null,
...taskHalPlaybackPatch,
@@ -3939,6 +3965,57 @@ function isTaskHalRunPausedByOperator(state = {}) {
|| state.machine?.taskPaused === true;
}
function programControlModeForMachine(machine = {}) {
return machine.mode === "mdi" ? "mdi" : "auto";
}
function createPowerToggleMachinePatch(machine = {}, turningOn = false) {
return {
...machine,
powerOn: turningOn,
estopActive: false,
taskState: turningOn ? "on" : "off",
manualPanel: "manual",
allHomed: turningOn ? Boolean(machine.allHomed) : false,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
};
}
function createTaskHalPauseLock(state = {}, source = "pause") {
return {
apiName: "web-rtcp-5axis-task-hal-pause-lock",
active: true,
source,
createdAt: new Date().toISOString(),
runState: "paused",
interpState: "paused",
sampleIndex: Number(state.programExecutionSampleIndex || 0),
motionIndex: Number(state.programExecutionMotionIndex || 0),
activeLine: Number(state.activeLine || state.programStartLine || 1),
axisPose: clampAxisPoseToProfile(state.axisPose || initialAxisPose, state.profile),
tcpPose: {
x: Number(state.tcpPose?.x || 0),
y: Number(state.tcpPose?.y || 0),
z: Number(state.tcpPose?.z || 0),
},
semanticBoundary: "linuxcnc_axis_task_pauseresume_freezes_task_hal_status_until_resume",
};
}
function zeroProgramRuntimeVelocity(feedback) {
if (!feedback) return feedback;
return {
...feedback,
currentVelocityMmPerMin: 0,
requestedVelocityMmPerMin: 0,
distanceToGo: 0,
dtg: feedback.dtg ? { ...feedback.dtg, x: 0, y: 0, z: 0 } : feedback.dtg,
activeDepth: 0,
};
}
function createStoppedProgramStatePatch(state, {
reason = "stopped",
operatorMessage = "program stopped",
@@ -3951,6 +4028,7 @@ function createStoppedProgramStatePatch(state, {
taskPaused: false,
},
runState: reason === "aborted" ? "stopped" : reason,
taskHalPauseLock: null,
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
@@ -4058,6 +4136,10 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const motion = status?.motionStatus?.motion || {};
const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0);
const motionProgramLine = Number(motion.programLine || 0);
const interpState = normalizeTaskHalInterpState(ui.interpState || status?.task?.interpState);
const paused = interpState === "paused" || motion.paused === true;
const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0);
const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60;
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "linuxcnc-task-motion-hal-wasm",
@@ -4072,8 +4154,8 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
timeSeconds: Number(ui.taskCycle || 0) * 0.01,
axisPose,
currentVelocityMmPerMin: Number(ui.currentVelocity || 0),
requestedVelocityMmPerMin: Number(motion.requestedVel || 0) * 60,
currentVelocityMmPerMin: currentVelocity,
requestedVelocityMmPerMin: requestedVelocity,
distanceToGo: motion.inPosition === true ? 0 : 1,
dtg: { x: 0, y: 0, z: 0 },
queueDepth: Number(ui.motionQueueDepth || status?.motionStatus?.commandQueueDepth || 0),

View File

@@ -10,7 +10,7 @@ export const AXIS_BUTTON_PARITY = [
{ id: "menu-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" },
{ id: "menu-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine ON/OFF after estop reset" },
{ id: "menu-home-all", action: "home-all", sourceSymbol: "commands.home_all_joints", sourceLines: "axis.py:2586-2596", expected: "home all joints in manual mode" },
{ id: "menu-run-ready", action: "run-ready", sourceSymbol: "AXIS run preconditions", sourceLines: "axis.py:2308-2320", expected: "power on, home, auto mode and TCP kins ready" },
{ id: "menu-run-ready", action: "run-ready", sourceSymbol: "AXIS run preconditions", sourceLines: "axis.py:2379-2391", expected: "open/stage the selected program without substituting power, Home All, or Run" },
{ id: "menu-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "AUTO_RUN from current start line" },
{ id: "menu-pause", action: "pause", sourceSymbol: "commands.task_pause", sourceLines: "axis.py:2329-2333", expected: "AUTO_PAUSE when running" },
{ id: "menu-resume", action: "resume", sourceSymbol: "commands.task_resume", sourceLines: "axis.py:2344-2351", expected: "AUTO_RESUME when paused" },

View File

@@ -0,0 +1,703 @@
# AXIS 主控制按钮功能、先决条件与机床状态影响详解
本文档单独整理 `5axis-xyzbc-trt-sim` 配置在 AXIS 界面中常用主控制按钮的执行条件、状态影响和对应程序链路。目标配置为:
```text
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
```
该配置使用:
- GUI`[DISPLAY] DISPLAY = axis`
- 任务进程:`[TASK] TASK = milltask`
- HAL 仿真闭环:`[HAL] HALFILE = LIB:basic_sim.tcl`
- 默认打开程序:`[DISPLAY] OPEN_FILE = ./demos/xyzbc_switchkins.ngc`
- 运动学:`[KINS] KINEMATICS = xyzbc-trt-kins sparm=identityfirst`
- 关节数:`[KINS] JOINTS = 5`
## 1. 状态和模式基础
AXIS 主按钮最终通过 Python `linuxcnc.command()` 对象发送 NML 命令给 `milltask`。常见状态如下:
| 名称 | LinuxCNC 常量 | 含义 |
|---|---|---|
| 急停中 | `STATE_ESTOP` | 急停有效,机床关闭,运动禁止 |
| 急停已解除但未上电 | `STATE_ESTOP_RESET` | 急停链释放,机床仍未使能 |
| 下电 | `STATE_OFF` | 机床未使能Python 扩展说明中等价于急停复位后的 off 状态 |
| 上电 | `STATE_ON` | 急停释放且机床使能可回零、MDI、自动运行 |
常见任务模式如下:
| 名称 | LinuxCNC 常量 | 用途 |
|---|---|---|
| 手动 | `MODE_MANUAL` | 回零、点动、手动操作 |
| MDI | `MODE_MDI` | 执行单行 MDI 指令 |
| 自动 | `MODE_AUTO` | 执行已装载 G-code 程序 |
核心源码位置:
```text
src/emc/usr_intf/axis/scripts/axis.py
src/emc/usr_intf/axis/extensions/emcmodule.cc
src/emc/task/emctaskmain.cc
src/emc/task/emctask.cc
src/emc/task/taskintf.cc
src/emc/nml_intf/emc_nml.hh
```
## 2. 按钮总表
| 按钮/动作 | AXIS 回调函数 | Python API | NML 命令 | 主要先决条件 | 主要状态影响 |
|---|---|---|---|---|---|
| 急停 | `estop_clicked()` | `c.state(STATE_ESTOP)` | `EMC_TASK_SET_STATE` | LinuxCNC 已运行;通常任意非急停状态都可触发 | 中止运动、主轴、冷却;使能关闭;进入 `STATE_ESTOP` |
| 解除急停 | `estop_clicked()` | `c.state(STATE_ESTOP_RESET)` | `EMC_TASK_SET_STATE` | 当前为 `STATE_ESTOP` | 释放急停链;仍未上电;进入 `STATE_ESTOP_RESET` |
| 上电 | `onoff_clicked()` | `c.state(STATE_ON)` | `EMC_TASK_SET_STATE` | 当前为 `STATE_ESTOP_RESET` | 轨迹/运动使能;进入 `STATE_ON` |
| Home | `home_all_joints()` / `home_joint()` | `c.home(joint)` | `EMC_JOINT_HOME` | `STATE_ON`;解释器空闲;手动模式;没有正在回零的关节 | 指定关节或全部关节回零,`homed[]` 变为 true |
| 执行程序 | `task_run()` | `c.auto(AUTO_RUN, line)` | `EMC_TASK_PLAN_RUN` | 上电、已回零、程序已打开、通过运行前检查 | 切入自动模式,解释器进入 `READING`,开始运行 G-code |
| 暂停 | `task_pause()` | `c.auto(AUTO_PAUSE)` | `EMC_TASK_PLAN_PAUSE` | 自动模式;解释器处于 `READING``WAITING` | 轨迹暂停,解释器状态保存为 `PAUSED``task_paused=1` |
| 恢复暂停 | `task_resume()` / `task_pauseresume()` | `c.auto(AUTO_RESUME)` | `EMC_TASK_PLAN_RESUME` | 当前已暂停;自动或 MDI 模式;未被 `resume-inhibit` 禁止 | 轨迹恢复,解释器返回暂停前状态,`task_paused=0` |
| 单步执行 | `task_step()` | `c.auto(AUTO_STEP)` | `EMC_TASK_PLAN_STEP` | 自动模式且解释器空闲;若不是空闲会先做运行警告检查 | 打开 single step一次推进一个解释/运动步骤 |
## 3. 急停
### 3.1 执行入口
AXIS 中急停按钮和 `F1` 键使用同一个回调:
```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`,就是解除急停。
### 3.2 先决条件
- LinuxCNC 已启动AXIS 可读取状态并可向 command channel 发送命令。
- 不要求当前在手动、MDI 或自动模式。
- 不要求程序空闲;急停是最高优先级安全动作,可在运动中触发。
### 3.3 机床状态影响
急停发送:
```text
c.state(linuxcnc.STATE_ESTOP)
```
Python 扩展生成:
```text
EMC_TASK_SET_STATE(state = ESTOP)
```
`milltask` 调用:
```text
emcTaskSetState(EMC_TASK_STATE::ESTOP)
```
主要影响:
- `emcMotionAbort()`:立即中止当前运动。
- `emcSpindleAbort()`:中止主轴。
- `emcAuxEstopOn()`:置 IO 急停。
- `emcTrajDisable()`:关闭轨迹/运动使能。
- `emcCoolantFloodOff()`:关闭冷却。
- `emcTaskAbort()`:中止任务层正在执行的程序/MDI。
- `emcIoAbort(TASK_STATE_ESTOP)`:通知 IO 层急停原因。
- `emcJointUnhome(-2)`:仅清除配置为 volatile home 的关节回零状态。
- `emcTaskPlanSynch()`:同步解释器/任务计划状态。
在本仿真配置中,`basic_sim.tcl``motion.motion-enabled` 接到仿真伺服闭环选择信号。急停后 motion disabled仿真反馈保持当前位置不再跟随新的 `joint.N.motor-pos-cmd`
## 4. 解除急停
### 4.1 执行入口
仍然是 AXIS 的 `estop_clicked()`。当前状态为 `STATE_ESTOP` 时,按钮执行:
```text
c.state(linuxcnc.STATE_ESTOP_RESET)
```
### 4.2 先决条件
- 当前必须处于 `STATE_ESTOP`,否则同一个按钮会变成“急停”动作。
- 外部急停链、HAL 急停反馈需要允许复位。本仿真中 `basic_sim.tcl` 使用回环:
```text
iocontrol.0.user-enable-out -> iocontrol.0.emc-enable-in
```
所以没有真实硬件急停链阻塞。
### 4.3 机床状态影响
`emcTaskSetState(ESTOP_RESET)` 主要执行:
- `emcAuxEstopOff()`:释放急停。
- `emcCoolantFloodOff()`:保持冷却关闭。
- `emcTaskAbort()`:清理任务执行状态。
- `emcIoAbort(TASK_STATE_ESTOP_RESET)`:通知 IO 层状态变化。
- `emcSpindleAbort()`:确保主轴停止。
- `emcAbortCleanup()`:清理 abort 状态。
- `emcTaskPlanSynch()`:同步解释器。
解除急停后机床仍然没有上电,状态是:
```text
STATE_ESTOP_RESET
```
此时不能执行 Home、MDI 或自动运行,需要再按“上电”。
## 5. 上电
### 5.1 执行入口
AXIS 中上电/下电按钮和 `F2` 键使用:
```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` 时执行上电;否则执行下电。
### 5.2 先决条件
- 必须先解除急停,当前状态为 `STATE_ESTOP_RESET`
- 如果当前仍为 `STATE_ESTOP`,点击该按钮不会上电,而会进入 `STATE_OFF` 分支,实际无法达到可运动状态。
### 5.3 机床状态影响
上电发送:
```text
c.state(linuxcnc.STATE_ON)
```
`emcTaskSetState(ON)` 主要执行:
- `emcTrajEnable()`:向实时 motion 发送 `EMCMOT_ENABLE`
- `emcCoolantFloodOff()`:确保冷却默认关闭。
上电后:
- `task_state = STATE_ON`
- 运动系统使能。
- `motion.motion-enabled` 为真。
- 本仿真 HAL 中各关节 PID pass-through 被允许,`joint.N.motor-pos-cmd` 可通过仿真闭环更新到 `joint.N.motor-pos-fb`
### 5.4 下电补充
如果机器已经不是 `STATE_ESTOP_RESET`,同一个按钮会发送:
```text
c.state(linuxcnc.STATE_OFF)
```
下电影响包括中止运动、关闭轨迹使能、通知 IO、清理任务、同步解释器并清除 volatile home 关节的回零状态。
## 6. Home
### 6.1 执行入口
本配置 5 个关节都设置了:
```ini
HOME_SEARCH_VEL = 0
HOME_SEQUENCE = 0
```
AXIS 会显示 `Home All` 按钮。按钮执行:
```python
def home_all_joints(event=None):
if not manual_ok(): return
ensure_mode(linuxcnc.MODE_MANUAL)
...
go_home(-1)
```
`Home` 键可执行当前选择关节的 `home_joint()``Ctrl-Home` 执行全部回零。
`go_home()` 内部执行:
```python
set_motion_teleop(0)
c.home(num)
c.wait_complete()
```
其中 `num = -1` 表示全部关节。
### 6.2 先决条件
Home All 的主要条件:
- `manual_ok()` 为真:
- `task_state == STATE_ON`
- 解释器空闲,或者 MDI 队列仍允许用户动作
- AXIS 会切换到 `MODE_MANUAL`
- 当前没有任何关节正在回零;`go_home()` 会检查 `s.joint[j]["homing"]`
- 如果已经全部回零AXIS 会弹出确认提示。
单个关节 Home 的额外条件:
- 如果选择的是坐标轴字母,并且当前运动学不是 identityAXIS 会提示使用 joint mode 回零并拒绝直接按轴回零。
- 对重复坐标字母配置AXIS 禁止按轴单独回零。
本配置启动默认是 `sparm=identityfirst`,即 `switchkins-type = 0` 为 identity适合先回零。
### 6.3 机床状态影响
Home 发送:
```text
c.home(joint)
```
Python 扩展生成:
```text
EMC_JOINT_HOME(joint = -1 或具体关节号)
```
`milltask` 调用:
```text
emcJointHome(joint)
```
`taskintf.cc` 再向实时 motion 发送:
```text
EMCMOT_JOINT_HOME
```
本仿真使用 `basic_sim.tcl``simulated_home`,为每个关节建立:
```text
joint.N.home-sw-in
```
由于 `HOME_SEARCH_VEL = 0`,这是仿真中的立即/简化回零方式。完成后:
- 对应 `s.homed[N]` 变为 true。
- `Home All` 后 5 个关节均为已回零。
- 只有已回零后,普通配置才允许自动运行程序;`emctaskmain.cc``EMC_TASK_PLAN_RUN` 会检查 `all_homed()`
## 7. 执行程序
### 7.1 执行入口
AXIS 运行按钮和 `r` 键执行:
```python
def task_run(*event):
res = run_warn()
...
ensure_mode(linuxcnc.MODE_AUTO)
c.auto(linuxcnc.AUTO_RUN, program_start_line)
```
当前配置启动时自动打开:
```text
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc
```
该文件内容很短:
```ngc
o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]
m2
```
实际加工/演示逻辑在:
```text
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/xyzbc_switchkins_sub.ngc
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/helix_bc.ngc
```
### 7.2 先决条件
AXIS 层:
- 程序已经打开。
- `run_warn()` 运行前检查通过,或用户选择忽略软限位警告继续运行。
- AXIS 能切换到 `MODE_AUTO`
任务层:
- 机床需要处于可运行状态,通常为 `STATE_ON`
- 必须已经回零,除非配置关闭强制回零。`emctaskmain.cc``EMC_TASK_PLAN_RUN` 检查:
```text
if (!all_homed() && !no_force_homing) {
"Can't run a program when not homed"
}
```
### 7.3 机床状态影响
运行按钮发送:
```text
c.auto(linuxcnc.AUTO_RUN, program_start_line)
```
Python 扩展生成:
```text
EMC_TASK_PLAN_RUN(line = program_start_line)
```
任务层影响:
- 清除 single stepping
- `motion.traj.single_stepping = 0`
- `stepping = 0`
- 如果 task plan 尚未打开且有文件名,则打开当前 G-code 文件。
- 设置 `programStartLine`
- 设置解释器状态:
```text
interpState = READING
task_paused = 0
```
之后解释器读取 G-code生成 canonical commands`taskintf` 下发给 realtime motionmotion 在伺服周期中计算轨迹和各 joint 命令。
### 7.4 对当前演示程序的影响
`xyzbc_switchkins_sub.ngc` 会在四个象限重复执行:
1. `M429` 切换到 identity。
2. `G53 G0 X0 Y0 Z... B0 C0` 回到机床坐标安全姿态。
3. `G10 L20 P0 ...` 设置 G54。
4. 移动到当前象限中心。
5. 调用 `helix_bc` 执行带 B/C 姿态的螺旋轨迹。
其中 `M428/M429/M430` 是 remap
- `M429``motion.switchkins-type = 0`identity。
- `M428``motion.switchkins-type = 1`xyzbc TRT。
- `M430``motion.switchkins-type = 2`userk。
remap 通过:
```ngc
M68 E3 Q#<kinstype>
M66 E0 L0
```
把数值写入 `motion.analog-out-03`,再由 INI 中 HAL 网络连接到:
```text
motion.switchkins-type
```
## 8. 暂停
### 8.1 执行入口
AXIS 暂停按钮和 `p` 键执行:
```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)
```
### 8.2 先决条件
- 当前必须在 `MODE_AUTO`
- 解释器状态必须是:
- `INTERP_READING`:正在读程序
- `INTERP_WAITING`:等待运动或 IO 完成
- 如果程序已经空闲、已经暂停、处于手动或 MDIAXIS 直接返回,不发送暂停命令。
### 8.3 机床状态影响
暂停发送:
```text
EMC_TASK_PLAN_PAUSE
```
任务层执行:
```text
emcTrajPause()
interpResumeState = 当前解释器状态
interpState = PAUSED
task_paused = 1
```
效果:
- 轨迹规划暂停。
- 已进入 motion 队列的运动按 LinuxCNC pause 逻辑停止/保持。
- AXIS 状态显示为 paused。
- 后续恢复会回到 `interpResumeState`
## 9. 恢复暂停
### 9.1 执行入口
AXIS 恢复按钮和 `s` 键执行:
```python
def task_resume(*event):
s.poll()
if not s.paused:
return
if s.task_mode not in (linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI):
return
ensure_mode(linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI)
c.auto(linuxcnc.AUTO_RESUME)
```
AXIS 的暂停/恢复合并按钮 `task_pauseresume()` 还会检查:
```text
resume-inhibit
```
如果该 HAL pin 为真,则禁止恢复。本配置中 AXIS 创建了 `axisui.resume-inhibit` 输入 pin`xyzbc-trt.ini` 没有把它接到其他信号,默认不阻塞恢复。
### 9.2 先决条件
- 当前必须已经暂停,即 `s.paused` 为真。
- 当前模式必须是 `MODE_AUTO``MODE_MDI`
- 对合并暂停/恢复按钮,`resume-inhibit` 不能为真。
### 9.3 机床状态影响
恢复发送:
```text
EMC_TASK_PLAN_RESUME
```
任务层执行:
```text
emcTrajResume()
interpState = interpResumeState
task_paused = 0
motion.traj.single_stepping = 0
stepping = 0
steppingWait = 0
```
效果:
- 轨迹恢复。
- 解释器继续从暂停点运行。
- 关闭单步状态,回到连续运行。
## 10. 单步执行
### 10.1 执行入口
AXIS 单步按钮和 `t` 键执行:
```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)
```
### 10.2 先决条件
常见使用方式:
- 机床上电。
- 已回零。
- 程序已打开。
- 当前可切换到 `MODE_AUTO`
- 如果不是自动空闲状态AXIS 会先清除高亮并执行 `run_warn()` 检查;如果用户取消或检查失败,则不单步。
任务层对第一次单步有特殊处理:
- 如果自动模式空闲时收到 `EMC_TASK_PLAN_STEP`,会先发起从第 0 行运行,再立即暂停轨迹,使程序进入单步逻辑。
### 10.3 机床状态影响
单步发送:
```text
EMC_TASK_PLAN_STEP
```
任务层按当前执行阶段处理:
- 在自动空闲初次单步时:
- 创建 `EMC_TASK_PLAN_RUN(line=0)`
- 执行 run
- 调用 `emcTrajPause()`
- 在解释器读取/等待/暂停过程中:
- `motion.traj.single_stepping = 1`
- `stepping = 1`
- `steppingWait = 0`
- 如果轨迹队列已有暂停运动,单步会推进队列中的下一步
效果:
- 每次点击只推进一个解释/运动步骤。
- 与“恢复暂停”不同,单步不会切回连续运行。
- 恢复暂停会清除 single stepping。
## 11. 当前 5 轴仿真配置的特殊注意点
### 11.1 启动默认不能直接运行
启动后通常处于急停或未上电状态。正确顺序是:
```text
解除急停 -> 上电 -> Home All -> 执行程序
```
如果未回零直接执行程序,任务层会报:
```text
Can't run a program when not homed
```
### 11.2 Home 与 switchkins 的关系
本配置 `sparm=identityfirst`,启动默认:
```text
motion.switchkins-type = 0
```
也就是 identity kinematics。回零应优先在 identity 下完成。演示程序内部也会反复使用 `M429` 回到 identity 后再做安全定位和坐标设置。
### 11.3 执行程序与 PyVCP 运动学按钮的关系
主运行按钮执行当前打开的 G-code。右侧 PyVCP 的:
- `IDENTITY`
- `TCP:XYZBC`
- `userk`
不是主运行控制按钮,它们通过 `switchkins_postgui.hal` 接到:
```text
halui.mdi-command-00 -> M429
halui.mdi-command-01 -> M428
halui.mdi-command-02 -> M430
```
这些按钮本质是发送 MDI 命令切换运动学,不负责运行、暂停或恢复程序。
### 11.4 仿真 HAL 对状态的响应
`basic_sim.tcl` 建立的关键闭环:
```text
iocontrol.0.user-enable-out -> iocontrol.0.emc-enable-in
motion.motion-enabled -> JN_mux.sel
joint.N.amp-enable-out -> JN_pid.enable
joint.N.motor-pos-cmd -> JN_pid.command
JN_pid.output -> JN_mux.in1
JN_mux.out -> joint.N.motor-pos-fb
```
因此:
- 急停/下电会关闭 motion enable反馈保持不继续跟随命令。
- 上电后 motion enable 打开,仿真伺服闭环重新允许命令传递到反馈。
- Home 通过 `sim_home_switch` 给每个 `joint.N.home-sw-in` 提供模拟 home switch。
## 12. 推荐操作流程
启动:
```bash
cd /home/mes123456/linuxcnc-master
scripts/rip-environment linuxcnc configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
```
标准执行顺序:
```text
1. 点击急停按钮或 F1解除急停使状态到 STATE_ESTOP_RESET。
2. 点击上电按钮或 F2进入 STATE_ON。
3. 点击 Home All 或 Ctrl-Home5 个关节全部回零。
4. 确认已打开 demos/xyzbc_switchkins.ngc。
5. 点击 Run 或按 r进入 MODE_AUTO 并执行程序。
6. 需要临时停止时按 Pause 或 p。
7. 需要继续时按 Resume 或 s。
8. 需要逐步观察时按 Step 或 t。
9. 任何异常情况下按 F1 急停。
```
## 13. 对应程序链路汇总
### 13.1 急停、解除急停、上电
```text
AXIS 按钮/F1/F2
-> axis.py estop_clicked() / onoff_clicked()
-> linuxcnc.command().state(...)
-> emcmodule.cc state()
-> EMC_TASK_SET_STATE
-> emctaskmain.cc emcTaskIssueCommand()
-> emctask.cc emcTaskSetState()
-> taskintf.cc emcTrajEnable/Disable, emcMotionAbort
-> realtime motion / IO / HAL 状态变化
```
### 13.2 Home
```text
AXIS Home All / Home Joint / Ctrl-Home / Home
-> axis.py home_all_joints() / home_joint()
-> go_home()
-> linuxcnc.command().home(...)
-> emcmodule.cc home()
-> EMC_JOINT_HOME
-> emctaskmain.cc emcJointHome()
-> taskintf.cc EMCMOT_JOINT_HOME
-> realtime motion homing
-> basic_sim.tcl simulated_home / sim_home_switch
-> joint.N.homed = true
```
### 13.3 运行、暂停、恢复、单步
```text
AXIS Run/Pause/Resume/Step
-> axis.py task_run/task_pause/task_resume/task_step
-> linuxcnc.command().auto(...)
-> emcmodule.cc emcauto()
-> EMC_TASK_PLAN_RUN / PAUSE / RESUME / STEP
-> emctaskmain.cc
-> RS274NGC interpreter / task planner / trajectory planner
-> realtime motion
-> HAL 仿真闭环
-> Vismach 与 AXIS 状态显示
```

View File

@@ -0,0 +1,694 @@
# AXIS 主界面暂停按钮 LinuxCNC 调用链分析
## 目的
本文分析 `/home/mes123456/cnc_wams/linuxcnc` 源码中 AXIS 主界面“暂停按钮”的完整调用链,包括 UI、Python 扩展、NML task 命令、task 状态机、motion 命令和轨迹规划器 TP 的实现。目标是为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的暂停按钮修正提供对标依据。
结论先行AXIS 的暂停不是只改一个 `runState`,而是同时维护两层暂停语义:
1. task/interpreter 暂停:`EMC_TASK_PLAN_PAUSE` 使 `task.interpState = PAUSED`,保存 `interpResumeState`,并设置 `task.task_paused = 1`
2. motion/trajectory 暂停task 调用 `emcTrajPause()`motion 收到 `EMCMOT_PAUSE` 后调用 `tpPause()`,轨迹规划器把 `tp->pausing = 1`,后续插补以速度目标 0 做受控减速,并通过 `motion.traj.paused` 对外反馈。
因此 Web 暂停按钮要同时冻结“解释器继续读行”和“运动采样继续推进”,并保留恢复前的解释器状态。
## 调用链总览
AXIS 工具栏暂停按钮链条:
```text
share/axis/tcl/axis.tcl
.toolbar.program_pause -command task_pauseresume
-> src/emc/usr_intf/axis/scripts/axis.py
commands.task_pauseresume()
-> linuxcnc.command().auto(AUTO_PAUSE 或 AUTO_RESUME)
-> src/emc/usr_intf/axis/extensions/emcmodule.cc
emcauto()
-> 发送 EMC_TASK_PLAN_PAUSE / EMC_TASK_PLAN_RESUME NML 命令
-> src/emc/task/emctaskmain.cc
emcTaskPlan() / emcTaskIssueCommand()
-> pause: emcTrajPause(); interpState=PAUSED; task_paused=1
-> resume: emcTrajResume(); interpState=interpResumeState; task_paused=0
-> src/emc/task/taskintf.cc
emcTrajPause() / emcTrajResume()
-> 写 EMCMOT_PAUSE / EMCMOT_RESUME 到 motion
-> src/emc/motion/command.c
EMCMOT_PAUSE: tpPause(); emcmotStatus->paused=1
EMCMOT_RESUME: tpResume(); emcmotStatus->paused=0
-> src/emc/tp/tp.c
tpPause(): tp->pausing=1
tpResume(): tp->pausing=0
```
菜单 Pause 和 Resume 是分开的命令;工具栏按钮是同一个 `task_pauseresume`,会根据当前状态决定暂停还是恢复。
## AXIS UI 层
### Tcl 工具栏按钮
`linuxcnc/share/axis/tcl/axis.tcl:543-549` 定义工具栏暂停按钮:
```tcl
Button .toolbar.program_pause \
-command task_pauseresume \
-helptext [_ "Pause \[P\] / resume \[S\] execution"] \
-image [load_image tool_pause]
```
这个按钮不是固定 Pause也不是固定 Resume而是 toggle 行为:运行时点击发送 pause已暂停时点击发送 resume。
同一文件 `axis.tcl:131-139` 定义菜单项:
```tcl
.menu.machine add command -accelerator P -command task_pause
.menu.machine add command -accelerator S -command task_resume
```
也就是说:
- 菜单 Pause 调 `task_pause`
- 菜单 Resume 调 `task_resume`
- 工具栏 Pause 图标调 `task_pauseresume`
Web 项目当前工具栏 `tbtn_pause` 对标的是 `task_pauseresume`,菜单 Pause/Resume 才应分别对标 `task_pause``task_resume`
### AXIS Python 命令入口
`linuxcnc/src/emc/usr_intf/axis/scripts/axis.py:2402-2407`
```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)
```
菜单 Pause 的门槛很严格:
- 必须是 `MODE_AUTO`
- `interp_state` 必须是 `INTERP_READING``INTERP_WAITING`
- 满足后发送 `AUTO_PAUSE`
`axis.py:2424-2431`
```python
def task_resume(*event):
s.poll()
if not s.paused:
return
if s.task_mode not in (linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI):
return
ensure_mode(linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI)
c.auto(linuxcnc.AUTO_RESUME)
```
菜单 Resume 的关键判断不是 `interp_state == PAUSED`,而是 `s.paused`。在 AXIS Python stat 中,`s.paused` 对应 motion/traj 层的 paused 状态,而 `s.task_paused` 是 task 层状态。两者都要关注。
`axis.py:2433-2443`
```python
def task_pauseresume(*event):
if s.task_mode not in (linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI):
return
ensure_mode(linuxcnc.MODE_AUTO, linuxcnc.MODE_MDI)
s.poll()
if s.paused:
if resume_inhibit: return
c.auto(linuxcnc.AUTO_RESUME)
elif s.interp_state != linuxcnc.INTERP_IDLE:
c.auto(linuxcnc.AUTO_PAUSE)
```
工具栏 toggle 的真实语义:
- 如果 motion/traj 已 paused发送 `AUTO_RESUME`
- 否则只要 interpreter 不是 idle发送 `AUTO_PAUSE`
- 如果 interpreter idle点击无效
- 允许 `MODE_AUTO``MODE_MDI`
- 恢复还会受 `resume_inhibit` 影响
`axis.py:906-910` 周期性把 LinuxCNC stat 写入 Tcl 变量:
- `vars.task_paused <- self.stat.task_paused`
- `vars.interp_pause <- self.stat.paused`
这说明 AXIS UI 同时观察 task 暂停和 motion 暂停。
键盘绑定在 `axis.py:3163-3165`
- `s` -> `commands.task_resume`
- `p` -> `commands.task_pause`
## Python 扩展和 NML 命令
AXIS 的 `linuxcnc.command().auto()` 在 C++ Python 扩展中实现。
`linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc:68-73` 定义本地 AUTO 常量:
```cpp
#define LOCAL_AUTO_RUN (0)
#define LOCAL_AUTO_PAUSE (1)
#define LOCAL_AUTO_RESUME (2)
#define LOCAL_AUTO_STEP (3)
```
`emcmodule.cc:2093-2129``emcauto()` 根据参数创建并发送 NML 命令:
- `LOCAL_AUTO_PAUSE` -> `EMC_TASK_PLAN_PAUSE`
- `LOCAL_AUTO_RESUME` -> `EMC_TASK_PLAN_RESUME`
- `LOCAL_AUTO_STEP` -> `EMC_TASK_PLAN_STEP`
关键片段:
```cpp
case LOCAL_AUTO_PAUSE:
emcSendCommand(s, pause);
break;
case LOCAL_AUTO_RESUME:
emcSendCommand(s, resume);
break;
case LOCAL_AUTO_STEP:
emcSendCommand(s, step);
break;
```
NML 类型定义在 `linuxcnc/src/emc/nml_intf/emc.hh:127-135`
- `EMC_TASK_PLAN_PAUSE_TYPE = 510`
- `EMC_TASK_PLAN_STEP_TYPE = 511`
- `EMC_TASK_PLAN_RESUME_TYPE = 512`
NML 消息类定义在 `linuxcnc/src/emc/nml_intf/emc_nml.hh:1276-1327`。这三个消息类没有额外字段,核心信息就是消息类型本身。
## LinuxCNC 状态字段
解释器状态定义在 `linuxcnc/src/emc/nml_intf/emc.hh:220-226`
```cpp
enum class EMC_TASK_INTERP {
IDLE = 1,
READING = 2,
PAUSED = 3,
WAITING = 4
};
```
motion/traj 状态字段在 `linuxcnc/src/emc/nml_intf/emc_nml.hh:965-991`
- `EMC_TRAJ_STAT::paused`
- `EMC_TRAJ_STAT::single_stepping`
- `EMC_TRAJ_STAT::queue`
- `EMC_TRAJ_STAT::activeQueue`
- `EMC_TRAJ_STAT::id`
task 状态字段在 `linuxcnc/src/emc/nml_intf/emc_nml.hh:1448-1471`
- `EMC_TASK_STAT::interpState`
- `EMC_TASK_STAT::task_paused`
AXIS Python 扩展把这些字段暴露给 Python
- `emcmodule.cc:1137` 暴露 `interp_state`
- `emcmodule.cc:1148` 暴露 `task_paused`
- `emcmodule.cc:3380-3383` 暴露 `INTERP_IDLE/READING/PAUSED/WAITING`
## task 层实现
`linuxcnc/src/emc/task/emctaskmain.cc:15-35` 说明 task 主循环原则:
1. 周期性调用 `emcTaskPlan()``emcTaskExecute()`
2. `emcTaskPlan()` 读取新命令,并按机器模式和状态决定处理方式。
3. AUTO 模式下解释器会把命令追加到 `interp_list`
4. `emcTaskExecute()` 根据 precondition/postcondition 从 `interp_list` 取命令执行。
5. immediate command 不走 interp list pre/postcondition。
这解释了一个关键现象:暂停可能作为 GUI 即时命令被处理,也可能作为解释器列表中的 pause/optional stop 被排队处理。
### 保存恢复状态
`emctaskmain.cc:427`
```cpp
static EMC_TASK_INTERP interpResumeState = EMC_TASK_INTERP::IDLE;
```
暂停时如果当前不是 PAUSED就保存当前解释器状态恢复时还原
- 暂停保存:`emctaskmain.cc:2337-2343`
- 恢复还原:`emctaskmain.cc:2369-2375`
### PLAN_PAUSE 执行点
`emctaskmain.cc:2337-2345`
```cpp
case EMC_TASK_PLAN_PAUSE_TYPE:
emcTrajPause();
if (emcStatus->task.interpState != EMC_TASK_INTERP::PAUSED) {
interpResumeState = emcStatus->task.interpState;
}
emcStatus->task.interpState = EMC_TASK_INTERP::PAUSED;
emcStatus->task.task_paused = 1;
retval = 0;
break;
```
这就是 task 层暂停的核心:
- 先暂停轨迹:`emcTrajPause()`
- 保存恢复前解释器状态:`interpResumeState`
- 设置解释器状态为 `PAUSED`
- 设置 `task_paused = 1`
### PLAN_RESUME 执行点
`emctaskmain.cc:2369-2377`
```cpp
case EMC_TASK_PLAN_RESUME_TYPE:
emcTrajResume();
emcStatus->task.interpState = interpResumeState;
emcStatus->task.task_paused = 0;
emcStatus->motion.traj.single_stepping = 0;
stepping = 0;
steppingWait = 0;
retval = 0;
break;
```
恢复时:
- 先恢复轨迹:`emcTrajResume()`
- 把解释器状态恢复到暂停前的 `interpResumeState`
-`task_paused`
- 清单步状态
### PAUSED 状态下不继续取解释器队列
`emctaskmain.cc:2614-2624``EMC_TASK_EXEC::DONE` 分支中只有当 `interpState != PAUSED` 时才继续从 `interp_list` 取下一条命令:
```cpp
if (!emcStatus->motion.traj.queueFull &&
emcStatus->task.interpState != EMC_TASK_INTERP::PAUSED) {
emcTaskCommand = interp_list.get();
}
```
这对 Web 很重要:暂停时不仅速度为 0还必须停止继续推进解释器命令和 UI sample。否则会出现“按钮显示暂停但 G-code 行号、sampleIndex 或刀具位置继续走”的问题。
### precondition 中的排队暂停
`emctaskmain.cc:1569-1572`
```cpp
case EMC_TASK_PLAN_PAUSE_TYPE:
case EMC_TASK_PLAN_OPTIONAL_STOP_TYPE:
return EMC_TASK_EXEC::WAITING_FOR_MOTION_AND_IO;
```
如果 pause 命令来自解释器列表,它会等前面的 motion 和 IO 完成后再执行。这不同于用户点击 GUI 时的 immediate pause。Web 如果模拟 M0/M1 或解释器排队暂停,应等当前 motion/IO 条件满足;如果模拟 AXIS 工具栏点击,则应立即向 task/motion 发暂停请求。
### STEP 与暂停的关系
`emctaskmain.cc:1254-1265`:在 PAUSED 状态下收到 STEP如果 motion queue 里有暂停的 motion就调用 `emcTrajStep()`,否则恢复 interpreter 到 `interpResumeState`
`emctaskmain.cc:2538-2549``STEPPING_CHECK()``stepping/steppingWait/steppedLine` 控制只执行一步。
这说明 STEP 不是普通 resume也不是永远推进一个 UI sample它是“在 paused 状态下短暂运行,直到 motion id/line 改变后再暂停”。
## task 到 motion 的接口
`linuxcnc/src/emc/task/taskintf.cc:1417-1422`
```cpp
int emcTrajPause()
{
emcmotCommand.command = EMCMOT_PAUSE;
return usrmotWriteEmcmotCommand(&emcmotCommand);
}
```
`taskintf.cc:1438-1449`
```cpp
int emcTrajStep()
{
emcmotCommand.command = EMCMOT_STEP;
return usrmotWriteEmcmotCommand(&emcmotCommand);
}
int emcTrajResume()
{
emcmotCommand.command = EMCMOT_RESUME;
return usrmotWriteEmcmotCommand(&emcmotCommand);
}
```
task 层不自己停止电机轨迹,它把 `EMCMOT_PAUSE/RESUME/STEP` 写给 motion。
motion 状态回读在 `taskintf.cc:1678`
```cpp
stat->paused = emcmotStatus.paused;
```
这就是 AXIS 的 `s.paused` 来源。
## motion 层实现
motion 命令枚举在 `linuxcnc/src/emc/motion/motion.h:94-104`
- `EMCMOT_PAUSE`
- `EMCMOT_REVERSE`
- `EMCMOT_FORWARD`
- `EMCMOT_RESUME`
- `EMCMOT_STEP`
motion 状态字段 `emcmotStatus->paused``motion.h:631-640` 定义,初始化在 `motion.c:940-951` 清零。
`linuxcnc/src/emc/motion/command.c:1234-1240`
```c
case EMCMOT_PAUSE:
tpPause(&emcmotInternal->coord_tp);
emcmotStatus->paused = 1;
break;
```
`command.c:1256-1263`
```c
case EMCMOT_RESUME:
emcmotStatus->stepping = 0;
tpResume(&emcmotInternal->coord_tp);
emcmotStatus->paused = 0;
break;
```
`command.c:1265-1274`
```c
case EMCMOT_STEP:
if(emcmotStatus->paused) {
emcmotInternal->idForStep = emcmotStatus->id;
emcmotStatus->stepping = 1;
tpResume(&emcmotInternal->coord_tp);
emcmotStatus->paused = 1;
}
```
STEP 的 motion 语义是:在 paused 时记录当前 motion id短暂 resume TP但状态仍视为 paused等 id 改变后自动再 pause。
`linuxcnc/src/emc/motion/control.c:2211-2218`
```c
if (emcmotStatus->stepping && emcmotInternal->idForStep != emcmotStatus->id) {
tpPause(&emcmotInternal->coord_tp);
emcmotStatus->stepping = 0;
emcmotStatus->paused = 1;
}
```
这就是 STEP 自动回到暂停的实现。
## 轨迹规划器 TP 实现
TP 结构字段在 `linuxcnc/src/emc/tp/tp_types.h:117-126`
```c
int nextId;
int execId;
int done;
int depth;
int activeDepth;
int aborting;
int pausing;
int reverse_run;
```
TP API 在 `linuxcnc/src/emc/tp/tp.h:45-48`
```c
int tpRunCycle(TP_STRUCT * tp, long period);
int tpPause(TP_STRUCT * tp);
int tpResume(TP_STRUCT * tp);
int tpAbort(TP_STRUCT * tp);
```
`linuxcnc/src/emc/tp/tp.c:4225-4240`
```c
int tpPause(TP_STRUCT * const tp)
{
if (0 == tp) return TP_ERR_FAIL;
tp->pausing = 1;
return TP_ERR_OK;
}
int tpResume(TP_STRUCT * const tp)
{
if (0 == tp) return TP_ERR_FAIL;
tp->pausing = 0;
return TP_ERR_OK;
}
```
暂停本质是设置 `tp->pausing`。真正的减速逻辑在规划循环里体现。
`tp.c:247-258`
```c
bool pausing = tp->pausing && (tc->synchronized == TC_SYNC_NONE || tc->synchronized == TC_SYNC_VELOCITY);
if (pausing) {
return 0.0;
}
```
暂停时 feed scale 返回 0使目标速度降到 0。
`tp.c:2832-2838`
```c
bool is_pausing = tp->pausing && (...);
bool use_velocity_control = (is_pausing || is_aborting || emcmotStatus->net_feed_scale <= TP_VEL_EPSILON);
```
暂停/abort/feed override 为 0 都进入 velocity-control 风格的减速规划。
`tp.c:4132-4135` 注释说明如果 aborting 或 pausing 且速度到 0就不需要继续规划并返回 stopped。
所以 LinuxCNC 的 motion pause 不是把位置瞬间锁死,也不是清空队列,而是让当前规划器以受控方式减速到 0到达 paused 稳态后,`current_vel` 为 0队列和当前 id 保留,恢复时从原队列继续。
## Web 当前实现的对照问题
从当前 Web 项目看,相关文件主要是:
- `app/src/ui/axis-shell.js`
- `app/src/state/linuxcnc-task-policy.js`
- `app/src/state/store.js`
Web 工具栏暂停按钮在 `axis-shell.js:170-182` 渲染,`tbtn_pause` 绑定 action `pause-resume``axis-shell.js:736-748` 将其分发为 `PAUSE_RESUME`,这与 AXIS 工具栏对标方向正确。
Web 当前 `isProgramPaused()``axis-shell.js:616-620` 使用:
```js
state.runState === "paused" ||
state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true
```
这少了一个 LinuxCNC 真实关键字段motion/traj paused也就是 AXIS 的 `s.paused`。Web 如果只看 `taskPaused``interpState`,可能无法准确模拟“运动层已经 paused但 task 状态尚未完全同步”的短窗口。
Web policy 在 `linuxcnc-task-policy.js:161-188` 对 PAUSE/RESUME 的 gating 大体接近 AXIS
- 菜单 PAUSE 要求 task on、auto、interp reading/waiting。
- pause-resume 允许 auto/mdiinterp idle 时忽略。
- RESUME 要求 interp paused。
但 AXIS `task_resume()``task_pauseresume()` 的恢复判断用的是 `s.paused`,即 motion/traj paused。Web 当前恢复更多依赖 `interpState === "paused"`。建议增加或统一 `machine.motionPaused` / `programRuntimeFeedback.paused` 字段,并将 resume gate 改为:
```text
canResume = taskState on
&& taskMode in auto/mdi
&& (motionPaused || interpState paused || taskPaused)
&& !resumeInhibit
```
Web `store.js:1760-1807` 的 PAUSE 分支已经设置:
- `interpState: "paused"`
- `taskPaused: true`
- `runState: "paused"`
- `feed.currentVelocity: 0`
- `programRuntimeFeedback` velocity 归零
但要严格对标 LinuxCNC还需要确保暂停后不再推进
- `programExecutionSampleIndex`
- `programUiExecution.sampleIndex`
- `programRuntimeFeedback.sampleIndex`
- `activeLine/currentLine`
- `axisPose`
- `DRO`
- `toolhead/toolAxis`
- task/HAL status loop 的采样推进
LinuxCNC 的关键保护是 `emctaskmain.cc:2614-2624``interpState == PAUSED` 时不从 `interp_list` 取下一条。Web 也需要同等约束:所有周期性 playback/status loop 在 paused 时必须只 poll 状态,不推进 sample 或解释器队列。
## Web 修正建议
### 1. 拆清三种状态
建议 Web 状态显式拆成:
```js
machine: {
interpState: "idle" | "reading" | "paused" | "waiting",
interpResumeState: "idle" | "reading" | "waiting",
taskPaused: boolean,
motionPaused: boolean,
resumeInhibit: boolean
}
```
其中:
- `taskPaused` 对标 `EMC_TASK_STAT::task_paused`
- `motionPaused` 对标 `EMC_TRAJ_STAT::paused` / AXIS `s.paused`
- `interpState` 对标 `EMC_TASK_STAT::interpState`
- `interpResumeState` 对标 task 层静态变量 `interpResumeState`
### 2. 工具栏 toggle 必须按 AXIS 判断
对标 `axis.py:2433-2443`
```js
if (taskMode not in auto/mdi) return;
if (motionPaused || taskPaused || interpState === "paused") {
if (resumeInhibit) return;
dispatch RESUME;
} else if (interpState !== "idle") {
dispatch PAUSE;
}
```
其中恢复优先看 `motionPaused`,不是只看 `interpState`
### 3. PAUSE 动作必须先冻结 motion再冻结 interpreter
对标 `emctaskmain.cc:2337-2343`
```js
motionPaused = true;
if (interpState !== "paused") interpResumeState = interpState;
interpState = "paused";
taskPaused = true;
runState = "paused";
```
同时不应修改当前 `sampleIndex`、当前行、DRO 或 toolhead 位置,只把速度反馈归零。
### 4. RESUME 动作必须恢复到 interpResumeState
对标 `emctaskmain.cc:2369-2375`
```js
motionPaused = false;
interpState = interpResumeState || "reading";
taskPaused = false;
singleStepping = false;
runState = interpState === "reading" ? "running" : "idle";
```
如果 `interpResumeState``waiting`,恢复后应回到 waiting再由执行循环完成后转 idle不要一律改成 reading。
### 5. STEP 不是普通 sample+1
对标 `command.c:1265-1274``control.c:2211-2218`
Web STEP 应记录当前 motion id/sample id短暂允许推进到下一个 motion id 或下一条解释器线,然后重新置:
- `interpState = "paused"`
- `taskPaused = true`
- `motionPaused = true`
- `runState = "paused"` 或短暂 `"stepping"` 后回 `"paused"`
当前 Web 的 `STEP` 分支会推进 sample这个方向可以保留但结束状态必须回到 paused并且不应变成长期 running。
### 6. 暂停时 status loop 只能刷新状态,不可推进执行
Web 的 task/HAL status loop 和 fallback playback 都要加硬条件:
```js
if (state.machine.interpState === "paused" ||
state.machine.taskPaused ||
state.machine.motionPaused ||
state.runState === "paused") {
return freezeCurrentExecutionPatch(state);
}
```
冻结 patch 应保留当前 sample/pose/DRO/toolhead只允许
- velocity -> 0
- paused flags -> true
- operator message/status timestamp 更新
## 验收断言建议
暂停按钮要通过以下断言才算对标 LinuxCNC
1. 工具栏点击运行中程序后:
- `runState === "paused"`
- `machine.interpState === "paused"`
- `machine.taskPaused === true`
- `machine.motionPaused === true`
- `feed.currentVelocity === 0`
2. 暂停后保持 5 秒:
- `sampleIndex` 不变
- `activeLine/currentLine` 不变
- `axisPose` 不变
- DRO 不变
- canvas toolhead/tool axis 不变
- runtime TCP 不变
3. 再点工具栏暂停按钮:
-`resumeInhibit` false则恢复到 `interpResumeState`
- `motionPaused === false`
- `taskPaused === false`
4. 菜单 Pause 在 `interpState === "idle"` 时必须无效。
5. 菜单 Resume 在 `motionPaused/taskPaused/interpState paused` 之外必须无效。
6. STEP 后必须重新进入 paused 稳态,不允许持续 running。
## 最小调用链索引
| 层级 | 文件 | 关键行 | 作用 |
| --- | --- | --- | --- |
| Tcl UI | `linuxcnc/share/axis/tcl/axis.tcl` | 543-549 | 工具栏 pause/resume 按钮绑定 `task_pauseresume` |
| 菜单 UI | `linuxcnc/share/axis/tcl/axis.tcl` | 131-139 | 菜单 Pause/Resume 分别绑定 `task_pause`/`task_resume` |
| AXIS Python | `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py` | 2402-2443 | 判断状态并调用 `c.auto(AUTO_PAUSE/RESUME)` |
| Python stat | `axis.py` | 906-910 | 更新 `task_paused``interp_pause` UI 变量 |
| Python 扩展 | `linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc` | 2093-2129 | `AUTO_PAUSE/RESUME/STEP` 转 NML 命令 |
| NML 类型 | `linuxcnc/src/emc/nml_intf/emc.hh` | 127-135 | 定义 `EMC_TASK_PLAN_PAUSE/RESUME/STEP_TYPE` |
| NML 类 | `linuxcnc/src/emc/nml_intf/emc_nml.hh` | 1276-1327 | 定义 task pause/resume/step 消息类 |
| task 状态 | `linuxcnc/src/emc/nml_intf/emc.hh` | 220-226 | 定义 `IDLE/READING/PAUSED/WAITING` |
| task 主循环 | `linuxcnc/src/emc/task/emctaskmain.cc` | 15-35 | 说明 plan/execute、interp_list、immediate command 架构 |
| task pause | `linuxcnc/src/emc/task/emctaskmain.cc` | 2337-2345 | 调 `emcTrajPause`,设置 `interpState/task_paused` |
| task resume | `linuxcnc/src/emc/task/emctaskmain.cc` | 2369-2377 | 调 `emcTrajResume`,恢复 `interpResumeState` |
| task execute freeze | `linuxcnc/src/emc/task/emctaskmain.cc` | 2614-2624 | PAUSED 时不继续取 `interp_list` |
| task->motion | `linuxcnc/src/emc/task/taskintf.cc` | 1417-1449 | 发送 `EMCMOT_PAUSE/STEP/RESUME` |
| motion command | `linuxcnc/src/emc/motion/command.c` | 1234-1274 | 调 `tpPause/tpResume`,维护 `emcmotStatus->paused` |
| motion step | `linuxcnc/src/emc/motion/control.c` | 2211-2218 | STEP 运动 id 改变后自动再 pause |
| TP API | `linuxcnc/src/emc/tp/tp.c` | 4225-4240 | `tpPause/tpResume` 设置 `tp->pausing` |
| TP 减速 | `linuxcnc/src/emc/tp/tp.c` | 247-258, 2832-2838 | pausing 时 feed scale/velocity control 使速度目标为 0 |
## 最终判断
LinuxCNC AXIS 暂停按钮可靠的根本原因是UI 只发命令task 层负责解释器状态和队列冻结motion 层负责轨迹暂停TP 层负责受控减速到 0。Web 项目的暂停如果“一直不好使用”,通常不是按钮绑定本身的问题,而是没有完整复现这四层状态同步:
- 没有 `motionPaused` 等价字段或没有用它做恢复判断;
- 暂停后 status/playback loop 仍推进 sample
- `interpResumeState` 没有严格保留和恢复;
- STEP 被实现成普通推进,而不是短暂恢复后再暂停;
- 菜单 Pause、菜单 Resume、工具栏 Pause/Resume toggle 的门槛混在一起。
修正 Web 时应优先把暂停状态机改成 LinuxCNC 的双层模型:`task_paused/interpState` 管解释器,`motionPaused/currentVelocity` 管运动,执行循环在 paused 时冻结取样与队列推进。

View File

@@ -283,11 +283,37 @@
await click('[data-action="estop"]', "AXIS estop");
await waitState((state) => state.machine.estopActive === true && state.runState === "estopped", "estop active");
await click('[data-action="power"]', "AXIS power while ESTOP");
await waitState((state) => (
state.machine.powerOn === false
&& state.machine.taskState === "off"
&& state.runState === "powered-off"
), "power button sends OFF while ESTOP");
await click('[data-action="power"]', "AXIS power while OFF");
await waitState((state) => (
state.machine.powerOn === false
&& state.machine.taskState === "off"
), "power button remains OFF until ESTOP reset");
await click('[data-action="estop"]', "AXIS estop before reset");
await waitState((state) => state.machine.estopActive === true && state.machine.taskState === "estop", "estop before reset");
await click('[data-action="estop"]', "AXIS estop reset");
await waitState((state) => state.machine.estopActive === false && state.machine.taskState === "estop-reset", "estop reset");
await click('[data-action="run"]', "AXIS run blocked before power");
await waitState((state) => (
state.machine.powerOn === false
&& state.runState === "idle"
&& state.operatorMessage === "run blocked: machine must be on"
), "run blocked before power");
await click('[data-action="power"]', "AXIS power");
await waitState((state) => state.machine.powerOn === true && state.machine.taskState === "on", "machine power on");
await click('[data-action="run"]', "AXIS run blocked before home");
await waitState((state) => (
state.machine.powerOn === true
&& state.machine.allHomed === false
&& state.runState === "idle"
&& state.operatorMessage === "run blocked: home machine first"
), "run blocked before home");
await click('[data-action="home-all"]', "AXIS home all");
await waitState((state) => (
state.machine.allHomed === true
@@ -473,7 +499,12 @@
await waitState((state) => Number(state.programAxisPreviewPath?.sampleCount || 0) > 0 && state.preview.pathPoints > 0, "reload restored real path samples");
doc.querySelector('[data-menu-command="run-ready"]').click();
await waitState((state) => state.machine.powerOn === true && state.machine.allHomed === true && state.machine.mode === "auto", "run-ready menu");
await waitState((state) => (
state.machine.powerOn === true
&& state.machine.allHomed === true
&& state.machine.mode === "manual"
&& state.operatorMessage === "RUN ready: program opened; press Run"
), "run-ready menu");
const beforeRun = api.getState();
await click('[data-action="run"]', "AXIS run");
const runState = await waitState((state) => (

View File

@@ -226,7 +226,19 @@ assert.equal(store.getState().programAxisPreviewPath.samples[0].tool.diameter, 8
assert.equal(store.getState().programAxisPreviewPath.samples[0].sourceFile, "xyzbc_switchkins_sub.ngc");
assert.equal(store.getState().programAxisPreviewPath.samples[0].statement.includes("g53 g0"), true);
store.dispatch({ type: "RUN_READY" });
store.dispatch({ type: "RUN" });
assert.equal(store.getState().machine.powerOn, false);
assert.equal(store.getState().machine.allHomed, false);
assert.equal(store.getState().machine.mode, "manual");
assert.equal(store.getState().operatorMessage, "RUN setup ready: reset ESTOP, power on, Home All, then Run");
await store.dispatch({ type: "RUN_FROM_OPERATOR" });
assert.equal(store.getState().runState, "idle");
assert.equal(store.getState().operatorMessage, "run blocked: machine must be on");
store.dispatch({ type: "TOGGLE_POWER" });
await store.dispatch({ type: "RUN_FROM_OPERATOR" });
assert.equal(store.getState().runState, "idle");
assert.equal(store.getState().operatorMessage, "run blocked: home machine first");
store.dispatch({ type: "HOME" });
await store.dispatch({ type: "RUN_FROM_OPERATOR" });
const liveRunState = store.getState();
assert.equal(liveRunState.programUiExecution.source, "programAxisPreviewPath.samples");
assert.equal(liveRunState.programUiExecution.samplePeriodMs, 50);
@@ -299,6 +311,21 @@ assert.equal(vismachState.transforms.tool.translate.z, -25);
assert.equal(vismachState.halNets.length, 9);
assert.equal(vismachState.semanticBoundary, "web_threejs_vismach_equivalent_driven_by_xyzbc_trt_hal_pins");
const pausedVismachState = buildVismachModelState({
...state,
runState: "paused",
machine: { ...state.machine, interpState: "paused", taskPaused: true },
axisPose: { x: 12.5, y: -6.25, z: 3.75, b: 35, c: -90 },
programRuntimeFeedback: {
axisPose: { x: 99, y: 88, z: 77, b: 66, c: 55 },
},
});
assert.equal(pausedVismachState.pins["table-x"], 12.5);
assert.equal(pausedVismachState.pins["saddle-y"], -6.25);
assert.equal(pausedVismachState.pins["spindle-z"], 3.75);
assert.equal(pausedVismachState.pins["tilt-b"], 35);
assert.equal(pausedVismachState.pins["rotate-c"], -90);
const stagedState = store.getState();
assert.equal(storeStage.save.profileId, "xyzbc-trt");
assert.equal(stagedState.machineFileStaging.profileId, "xyzbc-trt");
@@ -333,10 +360,26 @@ for (const action of [
const buttonStore = createSimulationStore();
buttonStore.dispatch({ type: "ESTOP" });
assert.equal(buttonStore.getState().machine.estopActive, true);
buttonStore.dispatch({ type: "TOGGLE_POWER" });
assert.equal(buttonStore.getState().machine.taskState, "off");
assert.equal(buttonStore.getState().machine.powerOn, false);
buttonStore.dispatch({ type: "TOGGLE_POWER" });
assert.equal(buttonStore.getState().machine.taskState, "off");
assert.equal(buttonStore.getState().machine.powerOn, false);
await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" });
assert.equal(buttonStore.getState().runState, "powered-off");
assert.equal(buttonStore.getState().operatorMessage, "run blocked: machine must be on");
buttonStore.dispatch({ type: "ESTOP" });
buttonStore.dispatch({ type: "RESET" });
assert.equal(buttonStore.getState().machine.taskState, "estop-reset");
await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" });
assert.equal(buttonStore.getState().runState, "idle");
assert.equal(buttonStore.getState().operatorMessage, "run blocked: machine must be on");
buttonStore.dispatch({ type: "TOGGLE_POWER" });
assert.equal(buttonStore.getState().machine.powerOn, true);
await buttonStore.dispatch({ type: "RUN_FROM_OPERATOR" });
assert.equal(buttonStore.getState().runState, "idle");
assert.equal(buttonStore.getState().operatorMessage, "run blocked: home machine first");
buttonStore.dispatch({ type: "HOME" });
assert.equal(buttonStore.getState().machine.allHomed, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.joint.x, 0);
@@ -350,12 +393,14 @@ if (buttonStore.getState().runState === "running") {
buttonStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(buttonStore.getState().runState, "paused");
assert.equal(buttonStore.getState().machine.interpState, "paused");
assert.equal(buttonStore.getState().feed.currentVelocity, 0);
buttonStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(buttonStore.getState().runState, "running");
assert.equal(buttonStore.getState().machine.interpState, "reading");
buttonStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(buttonStore.getState().runState, "paused");
assert.equal(buttonStore.getState().machine.interpState, "paused");
assert.equal(buttonStore.getState().feed.currentVelocity, 0);
const beforeStepSample = buttonStore.getState().programExecutionSampleIndex;
buttonStore.dispatch({ type: "STEP" });
assert.equal(buttonStore.getState().runState, "stepping");
@@ -367,11 +412,19 @@ if (buttonStore.getState().runState === "running") {
buttonStore.dispatch({ type: "PAUSE" });
assert.equal(buttonStore.getState().runState, "paused");
assert.equal(buttonStore.getState().machine.interpState, "paused");
assert.equal(buttonStore.getState().feed.currentVelocity, 0);
buttonStore.dispatch({ type: "RESUME" });
assert.equal(buttonStore.getState().runState, "running");
assert.equal(buttonStore.getState().machine.interpState, "reading");
}
buttonStore.dispatch({ type: "STOP" });
buttonStore.dispatch({ type: "TOGGLE_POWER" });
assert.equal(buttonStore.getState().machine.taskState, "off");
assert.equal(buttonStore.getState().machine.powerOn, false);
assert.equal(buttonStore.getState().machine.allHomed, false);
buttonStore.dispatch({ type: "TOGGLE_POWER" });
assert.equal(buttonStore.getState().machine.taskState, "off");
assert.equal(buttonStore.getState().machine.powerOn, false);
const idleInterpRunningStore = createSimulationStore({
runState: "running",
@@ -388,74 +441,147 @@ const idleInterpRunningStore = createSimulationStore({
},
});
idleInterpRunningStore.dispatch({ type: "PAUSE" });
assert.equal(idleInterpRunningStore.getState().runState, "paused");
assert.equal(idleInterpRunningStore.getState().machine.interpState, "paused");
assert.equal(idleInterpRunningStore.getState().machine.taskPaused, true);
idleInterpRunningStore.dispatch({ type: "RESUME" });
assert.equal(idleInterpRunningStore.getState().runState, "running");
assert.equal(idleInterpRunningStore.getState().machine.interpState, "reading");
buttonStore.dispatch({ type: "SET_MODE", mode: "manual" });
buttonStore.dispatch({ type: "SET_MODE", mode: "mdi" });
buttonStore.dispatch({ type: "RUN_MDI", command: "M428" });
assert.equal(buttonStore.getState().kinsType, "tcp-xyzbc");
assert.equal(buttonStore.getState().linuxCncProcessMonitor.axes.rtcpState, "on");
buttonStore.dispatch({ type: "RUN_MDI", command: "M429" });
assert.equal(buttonStore.getState().kinsType, "identity");
buttonStore.dispatch({ type: "RUN_MDI", command: "M430" });
assert.equal(buttonStore.getState().kinsType, "userk");
buttonStore.dispatch({ type: "SET_MODE", mode: "manual" });
buttonStore.dispatch({ type: "SET_ACTIVE_JOINT", joint: 4 });
const cBeforeJog = buttonStore.getState().axisPose.c;
buttonStore.dispatch({ type: "JOG", axis: "c", direction: 1, increment: 1 });
assert.equal(buttonStore.getState().axisPose.c, cBeforeJog + 1);
buttonStore.dispatch({ type: "RUN_MDI", command: "G10 L20 P0 C0", manualTouchOff: true });
assert.equal(buttonStore.getState().mdiHistory[0], "G10 L20 P0 C0");
assert.equal(buttonStore.getState().machine.mode, "manual");
assert.equal(buttonStore.getState().runState, "idle");
assert.equal(buttonStore.getState().operatorMessage, "manual touch off G10 L20 P0 C0");
buttonStore.dispatch({ type: "RUN_MDI", command: "G43", manualTouchOff: true });
assert.equal(buttonStore.getState().mdiHistory[0], "G43");
assert.equal(buttonStore.getState().machine.mode, "manual");
assert.equal(buttonStore.getState().runState, "idle");
const feedBefore = buttonStore.getState().feed.feedOverride;
buttonStore.dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: 10 });
assert.equal(buttonStore.getState().feed.feedOverride, feedBefore + 10);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.feed.feedOverridePercent, feedBefore + 10);
buttonStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" });
assert.equal(buttonStore.getState().spindle.direction, "reverse");
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.enabled, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600);
buttonStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" });
assert.equal(buttonStore.getState().spindle.direction, "stop");
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.enabled, false);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 0);
buttonStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" });
assert.equal(buttonStore.getState().spindle.direction, "forward");
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.enabled, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.actualRpm, 1600);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 1);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600);
buttonStore.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
assert.equal(buttonStore.getState().coolant.flood, true);
assert.equal(buttonStore.getState().linuxCncProcessMonitor.coolant.flood, true);
buttonStore.dispatch({ type: "SET_BLOCK_DELETE", enabled: true });
assert.equal(buttonStore.getState().gmoccapyGui.optionalBlocks, true);
buttonStore.dispatch({ type: "SET_OPTIONAL_STOP", enabled: true });
assert.equal(buttonStore.getState().gmoccapyGui.optionalStop, true);
buttonStore.dispatch({ type: "SET_IGNORE_LIMITS", enabled: true });
assert.equal(buttonStore.getState().gmoccapyGui.ignoreLimits, true);
buttonStore.dispatch({ type: "SET_VIEW", view: "x" });
assert.equal(buttonStore.getState().preview.selectedView, "x");
buttonStore.dispatch({ type: "CLEAR_PREVIEW" });
assert.equal(buttonStore.getState().preview.pathPoints, 0);
assert.equal(idleInterpRunningStore.getState().machine.interpState, "idle");
assert.equal(idleInterpRunningStore.getState().machine.taskPaused, false);
assert.equal(idleInterpRunningStore.getState().operatorMessage, "pause blocked: interpreter is not running");
idleInterpRunningStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(idleInterpRunningStore.getState().runState, "running");
assert.equal(idleInterpRunningStore.getState().machine.interpState, "idle");
assert.equal(idleInterpRunningStore.getState().operatorMessage, "pause ignored: interpreter is idle");
const staleManualRunningStore = createSimulationStore({
runState: "running",
machine: {
powerOn: true,
estopActive: false,
taskState: "on",
mode: "manual",
interpState: "reading",
interpResumeState: "reading",
taskPaused: false,
allHomed: true,
noForceHoming: false,
},
});
staleManualRunningStore.dispatch({ type: "PAUSE_RESUME" });
assert.equal(staleManualRunningStore.getState().runState, "running");
assert.equal(staleManualRunningStore.getState().machine.mode, "manual");
assert.equal(staleManualRunningStore.getState().machine.interpState, "reading");
assert.equal(staleManualRunningStore.getState().machine.taskPaused, false);
assert.equal(staleManualRunningStore.getState().operatorMessage, "pause blocked: task mode must be auto or MDI");
assert.equal(staleManualRunningStore.getState().taskHalPauseLock, null);
const lockedPauseState = staleManualRunningStore.getState();
staleManualRunningStore.dispatch({
type: "TASK_HAL_STATUS_APPLIED",
operatorMessage: "simulated stale running task/HAL status",
status: {
taskRuntimeReady: true,
taskCommandsDriveMotionRuntime: true,
task: {
state: "ON",
mode: "AUTO",
interpState: "READING",
execState: "EXEC",
taskCycle: 123,
},
motionStatus: {
cycle: 25,
motionHalSyncReady: true,
motion: {
programLine: Number(lockedPauseState.activeLine || 1) + 5,
currentVel: 12,
requestedVel: 12,
inPosition: false,
},
axis: {
x: 99,
y: 88,
z: 77,
b: 66,
c: 55,
},
},
halSnapshot: {
ready: true,
pins: {
"motion.program-line": { value: Number(lockedPauseState.activeLine || 1) + 5 },
},
},
},
});
assert.equal(staleManualRunningStore.getState().runState, "running");
assert.equal(staleManualRunningStore.getState().machine.mode, "manual");
assert.equal(staleManualRunningStore.getState().machine.interpState, "reading");
assert.equal(staleManualRunningStore.getState().taskHalPauseLock, null);
const controlStore = createSimulationStore();
controlStore.dispatch({ type: "ESTOP" });
controlStore.dispatch({ type: "RESET" });
controlStore.dispatch({ type: "TOGGLE_POWER" });
controlStore.dispatch({ type: "HOME" });
controlStore.dispatch({ type: "SET_MODE", mode: "manual" });
controlStore.dispatch({ type: "SET_MODE", mode: "mdi" });
controlStore.dispatch({ type: "RUN_MDI", command: "M428" });
assert.equal(controlStore.getState().kinsType, "tcp-xyzbc");
assert.equal(controlStore.getState().linuxCncProcessMonitor.axes.rtcpState, "on");
controlStore.dispatch({ type: "RUN_MDI", command: "M429" });
assert.equal(controlStore.getState().kinsType, "identity");
controlStore.dispatch({ type: "RUN_MDI", command: "M430" });
assert.equal(controlStore.getState().kinsType, "userk");
controlStore.dispatch({ type: "SET_MODE", mode: "manual" });
controlStore.dispatch({ type: "SET_ACTIVE_JOINT", joint: 4 });
const cBeforeJog = controlStore.getState().axisPose.c;
controlStore.dispatch({ type: "JOG", axis: "c", direction: 1, increment: 1 });
assert.equal(controlStore.getState().axisPose.c, cBeforeJog + 1);
controlStore.dispatch({ type: "RUN_MDI", command: "G10 L20 P0 C0", manualTouchOff: true });
assert.equal(controlStore.getState().mdiHistory[0], "G10 L20 P0 C0");
assert.equal(controlStore.getState().machine.mode, "manual");
assert.equal(controlStore.getState().runState, "idle");
assert.equal(controlStore.getState().operatorMessage, "manual touch off G10 L20 P0 C0");
controlStore.dispatch({ type: "RUN_MDI", command: "G43", manualTouchOff: true });
assert.equal(controlStore.getState().mdiHistory[0], "G43");
assert.equal(controlStore.getState().machine.mode, "manual");
assert.equal(controlStore.getState().runState, "idle");
const feedBefore = controlStore.getState().feed.feedOverride;
controlStore.dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: 10 });
assert.equal(controlStore.getState().feed.feedOverride, feedBefore + 10);
assert.equal(controlStore.getState().linuxCncProcessMonitor.feed.feedOverridePercent, feedBefore + 10);
controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" });
assert.equal(controlStore.getState().spindle.direction, "reverse");
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.enabled, true);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 1);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600);
controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" });
assert.equal(controlStore.getState().spindle.direction, "stop");
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.enabled, false);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 0);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 0);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 0);
controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" });
assert.equal(controlStore.getState().spindle.direction, "forward");
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.enabled, true);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.actualRpm, 1600);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.on, 1);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.forward, 1);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.reverse, 0);
assert.equal(controlStore.getState().linuxCncProcessMonitor.spindle.halPins.speedOut, 1600);
controlStore.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
assert.equal(controlStore.getState().coolant.flood, true);
assert.equal(controlStore.getState().linuxCncProcessMonitor.coolant.flood, true);
controlStore.dispatch({ type: "SET_BLOCK_DELETE", enabled: true });
assert.equal(controlStore.getState().gmoccapyGui.optionalBlocks, true);
controlStore.dispatch({ type: "SET_OPTIONAL_STOP", enabled: true });
assert.equal(controlStore.getState().gmoccapyGui.optionalStop, true);
controlStore.dispatch({ type: "SET_IGNORE_LIMITS", enabled: true });
assert.equal(controlStore.getState().gmoccapyGui.ignoreLimits, true);
controlStore.dispatch({ type: "SET_VIEW", view: "x" });
assert.equal(controlStore.getState().preview.selectedView, "x");
controlStore.dispatch({ type: "CLEAR_PREVIEW" });
assert.equal(controlStore.getState().preview.pathPoints, 0);
const stagingPlan = await createMachineFileStagingPlan({ profile });
assert.equal(stagingPlan.summary.remapFileCount >= 3, true);

View File

@@ -0,0 +1,302 @@
import { mkdir, writeFile } from "node:fs/promises";
import { createReadStream, statSync } from "node:fs";
import { createServer } from "node:http";
import { resolve } from "node:path";
import { chromium } from "../app/node_modules/playwright/index.mjs";
const repoRoot = resolve(import.meta.dirname, "../..");
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const appUrlPath = process.env.APP_URL_PATH || "/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html";
const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc";
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
const outputDir = resolve(projectRoot, "working/pause-position-traces", `pause-position-${timestamp}`);
const chromiumExecutable = process.env.CHROMIUM || findSystemChromium();
await mkdir(outputDir, { recursive: true });
const server = await startStaticServer(repoRoot);
let browser;
try {
browser = await chromium.launch({
headless: true,
executablePath: chromiumExecutable || undefined,
args: ["--disable-gpu", "--no-sandbox"],
});
const context = await browser.newContext({
viewport: { width: 1600, height: 1000 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
page.setDefaultTimeout(45000);
const url = `http://127.0.0.1:${server.port}${appUrlPath}`;
await page.goto(url, { waitUntil: "networkidle" });
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState));
await Promise.all([
page.evaluate(() => window.webRtcp5AxisSimulation.machineFileSeedReady),
page.evaluate(() => window.webRtcp5AxisSimulation.interpreterRuntimeReady),
page.evaluate(() => window.webRtcp5AxisSimulation.taskHalRuntimeReady),
]);
await page.evaluate((selectedSourceRel) => {
window.webRtcp5AxisSimulation.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: selectedSourceRel });
}, sourceRel);
await waitForState(page, (state) => (
state.machineFileStaging?.selectedGcodeSourceRel === sourceRel &&
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0
), "G-code loaded");
await ensureRunReady(page);
await page.click('[data-tool-id="btn_run"]');
await waitForState(page, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
Number(state.programExecutionSampleIndex || 0) >= 0
), "program running", 60000);
await page.waitForTimeout(1800);
const beforePause = await readTraceSample(page, "before-pause");
await page.click('[data-tool-id="tbtn_pause"]');
await waitForState(page, (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
), "pause button set paused");
const pauseStart = await readTraceSample(page, "pause-start");
const samples = [beforePause, pauseStart];
const startedAt = Date.now();
while (Date.now() - startedAt < 3500) {
await page.waitForTimeout(100);
samples.push(await readTraceSample(page, `pause-hold-${Date.now() - startedAt}`));
}
const analysis = analyzePauseTrace(samples);
const manifest = {
apiName: "xyzbc-trt-pause-position-json-trace",
status: analysis.positionChangedAfterPause ? "failed-position-changed" : "passed-position-frozen",
capturedAt: new Date().toISOString(),
appUrlPath,
url,
sourceRel,
outputDir,
pauseSelector: '[data-tool-id="tbtn_pause"]',
analysis,
samples,
};
await writeFile(resolve(outputDir, "trace.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
console.log(`pause_position_status=${manifest.status}`);
console.log(`trace=${resolve(outputDir, "trace.json")}`);
console.log(`app_url_path=${appUrlPath}`);
console.log(`position_changed=${analysis.positionChangedAfterPause}`);
console.log(`changed_fields=${analysis.changedFields.join(",")}`);
} finally {
await browser?.close().catch(() => {});
await server.close();
}
async function ensureRunReady(page) {
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "ESTOP" }));
await waitForState(page, (state) => state.machine?.taskState === "estop", "estop active");
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "RESET" }));
await waitForState(page, (state) => state.machine?.taskState === "estop-reset", "estop reset");
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "TOGGLE_POWER" }));
await waitForState(page, (state) => state.machine?.taskState === "on" && state.machine?.powerOn === true, "power on");
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "HOME" }));
await waitForState(page, (state) => state.machine?.allHomed === true, "home all");
}
async function readTraceSample(page, label) {
return page.evaluate((sampleLabel) => {
const state = window.webRtcp5AxisSimulation.getState();
const canvas = document.querySelector("[data-five-axis-canvas]");
const parseJson = (value) => {
try {
return value ? JSON.parse(value) : null;
} catch {
return null;
}
};
return {
label: sampleLabel,
capturedAt: new Date().toISOString(),
runState: state.runState,
taskState: state.machine?.taskState,
mode: state.machine?.mode,
interpState: state.machine?.interpState,
taskPaused: state.machine?.taskPaused,
activeLine: state.activeLine,
sampleIndex: state.programExecutionSampleIndex,
currentVelocity: state.feed?.currentVelocity,
axisPose: pickAxes(state.axisPose),
dro: pickDro(state.dro),
runtimeAxisPose: pickAxes(state.programRuntimeFeedback?.axisPose),
runtimeTcp: pickTcp(state.programRuntimeFeedback?.tcp),
uiExecution: {
status: state.programUiExecution?.status,
sampleIndex: state.programUiExecution?.sampleIndex,
sourceFile: state.programUiExecution?.sourceFile,
line: state.programUiExecution?.line,
statement: state.programUiExecution?.statement,
joint: pickAxes(state.programUiExecution?.joint),
tcp: pickTcp(state.programUiExecution?.tcp),
},
canvasToolhead: parseJson(canvas?.dataset?.threeToolhead),
canvasToolAxis: parseJson(canvas?.dataset?.threeToolAxis),
pauseButton: {
action: document.querySelector('[data-tool-id="tbtn_pause"]')?.dataset?.action || null,
paused: document.querySelector('[data-tool-id="tbtn_pause"]')?.dataset?.paused || null,
title: document.querySelector('[data-tool-id="tbtn_pause"]')?.title || null,
},
};
function pickAxes(value = {}) {
return value ? {
x: Number(value.x || 0),
y: Number(value.y || 0),
z: Number(value.z || 0),
a: Number(value.a || 0),
b: Number(value.b || 0),
c: Number(value.c || 0),
} : null;
}
function pickDro(value = {}) {
return value ? {
x: Number(value.x || 0),
y: Number(value.y || 0),
z: Number(value.z || 0),
a: Number(value.a || 0),
b: Number(value.b || 0),
c: Number(value.c || 0),
tcpX: Number(value.tcpX || 0),
tcpY: Number(value.tcpY || 0),
tcpZ: Number(value.tcpZ || 0),
} : null;
}
function pickTcp(value = {}) {
return value ? {
x: Number(value.x || 0),
y: Number(value.y || 0),
z: Number(value.z || 0),
} : null;
}
}, label);
}
function analyzePauseTrace(samples) {
const pauseSamples = samples.filter((sample) => sample.runState === "paused");
const baseline = pauseSamples[0] || null;
const changedFields = [];
if (baseline) {
for (const sample of pauseSamples.slice(1)) {
compareVector("axisPose", baseline.axisPose, sample.axisPose, changedFields);
compareVector("dro", baseline.dro, sample.dro, changedFields);
compareVector("runtimeAxisPose", baseline.runtimeAxisPose, sample.runtimeAxisPose, changedFields);
compareVector("runtimeTcp", baseline.runtimeTcp, sample.runtimeTcp, changedFields);
compareVector("uiExecution.joint", baseline.uiExecution?.joint, sample.uiExecution?.joint, changedFields);
compareVector("uiExecution.tcp", baseline.uiExecution?.tcp, sample.uiExecution?.tcp, changedFields);
compareVector("canvasToolhead", baseline.canvasToolhead, sample.canvasToolhead, changedFields, 1e-4);
if (Number(sample.sampleIndex) !== Number(baseline.sampleIndex)) changedFields.push("sampleIndex");
if (Number(sample.currentVelocity) !== 0) changedFields.push("currentVelocity");
}
}
return {
baselineLabel: baseline?.label || null,
pausedSampleCount: pauseSamples.length,
changedFields: [...new Set(changedFields)],
positionChangedAfterPause: changedFields.length > 0,
};
}
function compareVector(name, before, after, changedFields, tolerance = 1e-6) {
if (!before || !after) return;
for (const key of Object.keys(before)) {
const delta = Math.abs(Number(after[key] || 0) - Number(before[key] || 0));
if (delta > tolerance) changedFields.push(`${name}.${key}`);
}
}
async function waitForState(page, predicate, label, timeoutMs = 30000) {
const predicateText = predicate.toString();
await page.waitForFunction(
([source, selectedSourceRel]) => {
const state = window.webRtcp5AxisSimulation?.getState?.();
if (!state) return false;
const sourceRel = selectedSourceRel;
return Function("state", "sourceRel", `return (${source})(state, sourceRel);`)(state, sourceRel);
},
[predicateText, sourceRel],
{ timeout: timeoutMs },
).catch(async (error) => {
const state = await page.evaluate(() => window.webRtcp5AxisSimulation?.getState?.()).catch(() => null);
throw new Error(`Timed out waiting for ${label}: ${error.message}\n${JSON.stringify({
runState: state?.runState,
taskState: state?.machine?.taskState,
mode: state?.machine?.mode,
interpState: state?.machine?.interpState,
sampleIndex: state?.programExecutionSampleIndex,
operatorMessage: state?.operatorMessage,
}, null, 2)}`);
});
}
function startStaticServer(root) {
const server = createServer((request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
const decoded = decodeURIComponent(url.pathname);
const relative = decoded === "/" ? "/index.html" : decoded;
const target = resolve(root, `.${relative}`);
if (!target.startsWith(root)) {
response.writeHead(403);
response.end("Forbidden");
return;
}
let itemStat;
try {
itemStat = statSync(target);
if (!itemStat.isFile()) throw new Error("not a file");
} catch {
response.writeHead(404);
response.end("Not found");
return;
}
response.writeHead(200, {
"content-type": contentType(target),
"content-length": itemStat.size,
"cache-control": "no-store",
});
createReadStream(target).pipe(response);
});
return new Promise((resolveStart, rejectStart) => {
server.on("error", rejectStart);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
resolveStart({
port: address.port,
close: () => new Promise((resolveClose) => server.close(resolveClose)),
});
});
});
}
function contentType(file) {
if (file.endsWith(".html")) return "text/html; charset=utf-8";
if (file.endsWith(".js") || file.endsWith(".mjs")) return "text/javascript; charset=utf-8";
if (file.endsWith(".css")) return "text/css; charset=utf-8";
if (file.endsWith(".json")) return "application/json; charset=utf-8";
if (file.endsWith(".wasm")) return "application/wasm";
if (file.endsWith(".svg")) return "image/svg+xml";
if (file.endsWith(".png")) return "image/png";
return "application/octet-stream";
}
function findSystemChromium() {
for (const candidate of ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"]) {
try {
statSync(candidate);
return candidate;
} catch {
// Continue probing.
}
}
return null;
}

View File

@@ -0,0 +1,584 @@
import { mkdir, writeFile } from "node:fs/promises";
import { createReadStream, statSync } from "node:fs";
import { createServer } from "node:http";
import { resolve } from "node:path";
import { chromium } from "../app/node_modules/playwright/index.mjs";
const repoRoot = resolve(import.meta.dirname, "../..");
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const linuxCncSourceRoot = resolve(repoRoot, "linuxcnc");
const appUrlPath = "/web-rtcp-5axis-xyzbc-trt-sim-plan/app/index.html";
const sourceRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc";
const samplePeriodMs = 50;
const runStableBeforeFirstPauseMs = Math.max(Number(process.env.RUN_STABLE_BEFORE_FIRST_PAUSE_MS || 0), 0);
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
const outputDir = resolve(projectRoot, "working/screenshots", `estop-power-home-run-pause-50ms-${timestamp}`);
const chromiumExecutable = process.env.CHROMIUM || findSystemChromium();
await mkdir(outputDir, { recursive: true });
const server = await startStaticServer(repoRoot);
let browser;
const events = [];
const frames = [];
const assertions = [];
let capture = null;
let captureError = null;
try {
browser = await chromium.launch({
headless: true,
executablePath: chromiumExecutable || undefined,
args: ["--disable-gpu", "--no-sandbox"],
});
const context = await browser.newContext({
viewport: { width: 1600, height: 1000 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
page.setDefaultTimeout(45000);
await page.goto(`http://127.0.0.1:${server.port}${appUrlPath}`, { waitUntil: "networkidle" });
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState));
await Promise.all([
page.evaluate(() => window.webRtcp5AxisSimulation.machineFileSeedReady),
page.evaluate(() => window.webRtcp5AxisSimulation.interpreterRuntimeReady),
page.evaluate(() => window.webRtcp5AxisSimulation.taskHalRuntimeReady),
]);
await page.evaluate((selectedSourceRel) => {
window.webRtcp5AxisSimulation.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: selectedSourceRel,
});
}, sourceRel);
await waitForState(page, (state) => (
state.machineFileStaging?.selectedGcodeSourceRel === sourceRel &&
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0
), "xyzbc-trt real G-code loaded");
await page.waitForSelector("[data-five-axis-canvas]");
await assertCanvasHasPixels(page);
await ensureEstopActive(page);
await snapshot(page, "setup-estop-active");
capture = startFrameCapture(page);
await clickAndWait(page, '[data-action="estop"]', "解除 ESTOP", (state) => (
state.machine?.estopActive === false &&
state.machine?.taskState === "estop-reset" &&
state.runState === "idle"
));
await clickAndWait(page, '[data-action="power"]', "上电", (state) => (
state.machine?.powerOn === true &&
state.machine?.taskState === "on"
));
await clickAndWait(page, '[data-action="home-all"]', "Home All", (state) => (
state.machine?.allHomed === true &&
state.machine?.mode === "manual"
));
await clickAndWait(page, '[data-action="run"]', "Run", (state) => (
state.machine?.mode === "auto" &&
(state.runState === "running" || state.runState === "complete") &&
state.machine?.interpState === "reading" &&
state.programRuntimeFeedback
), 60000);
if (runStableBeforeFirstPauseMs > 0) {
await holdState(page, `Run 后稳定运行 ${runStableBeforeFirstPauseMs}ms`, runStableBeforeFirstPauseMs, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
), { requirePositionChange: true });
}
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次暂停", (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
));
await holdState(page, "第一次暂停保持 5 秒", 5000, (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
), { freezePosition: true });
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第一次继续执行", (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
await holdState(page, "第一次继续执行 10 秒", 10000, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次暂停", (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
));
await holdState(page, "第二次暂停保持 5 秒", 5000, (state) => (
state.runState === "paused" &&
state.machine?.interpState === "paused" &&
state.machine?.taskPaused === true
), { freezePosition: true });
await clickAndWait(page, '[data-tool-id="tbtn_pause"]', "第二次继续执行", (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
await holdState(page, "第二次继续执行 10 秒", 10000, (state) => (
state.runState === "running" &&
state.machine?.interpState === "reading" &&
state.machine?.taskPaused === false
));
clearInterval(capture.timer);
await capture.inFlight;
capture = null;
const finalState = await readStateSummary(page);
const manifest = {
apiName: "xyzbc-trt-estop-power-home-run-pause-50ms-verification",
status: "passed",
capturedAt: new Date().toISOString(),
projectRoot,
linuxCncSourceRoot,
appUrl: `http://127.0.0.1:${server.port}${appUrlPath}`,
linuxCncSourceReferences: [
resolve(linuxCncSourceRoot, "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"),
resolve(linuxCncSourceRoot, "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml"),
resolve(linuxCncSourceRoot, sourceRel),
resolve(linuxCncSourceRoot, "src/emc/task/emctaskmain.cc"),
resolve(linuxCncSourceRoot, "src/emc/nml_intf/emc.hh"),
],
sourceRel,
outputDir,
captureMethod: "Playwright clicked AXIS DOM controls for ESTOP reset, power, Home All, Run, pause/resume, while a 50ms interval full-page screenshot loop recorded the visible web simulator.",
samplePeriodMs,
runStableBeforeFirstPauseMs,
expectedSequence: [
"解除 ESTOP",
"上电",
"Home All",
"Run",
...(runStableBeforeFirstPauseMs > 0 ? [`Run 后稳定运行 ${runStableBeforeFirstPauseMs}ms`] : []),
"第一次暂停并保持 5 秒",
"第一次继续执行 10 秒",
"第二次暂停并保持 5 秒",
"第二次继续执行 10 秒",
],
events,
assertions,
capturedFrameCount: frames.length,
firstFrame: frames[0] || null,
lastFrame: frames[frames.length - 1] || null,
frames,
finalState,
};
await writeFile(resolve(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
console.log(`verification_status=passed`);
console.log(`screenshots=${outputDir}`);
console.log(`captured_frames=${frames.length}`);
console.log(`sample_period_ms=${samplePeriodMs}`);
console.log(`manifest=${resolve(outputDir, "manifest.json")}`);
} catch (error) {
captureError = error;
if (capture) {
clearInterval(capture.timer);
await capture.inFlight.catch(() => {});
}
const failedManifest = {
apiName: "xyzbc-trt-estop-power-home-run-pause-50ms-verification",
status: "failed",
capturedAt: new Date().toISOString(),
projectRoot,
linuxCncSourceRoot,
sourceRel,
outputDir,
error: error instanceof Error ? error.stack || error.message : String(error),
events,
assertions,
capturedFrameCount: frames.length,
firstFrame: frames[0] || null,
lastFrame: frames[frames.length - 1] || null,
frames,
};
await writeFile(resolve(outputDir, "manifest.json"), JSON.stringify(failedManifest, null, 2) + "\n", "utf8");
console.log(`verification_status=failed`);
console.log(`screenshots=${outputDir}`);
console.log(`captured_frames=${frames.length}`);
console.log(`manifest=${resolve(outputDir, "manifest.json")}`);
} finally {
await browser?.close().catch(() => {});
await server.close();
}
if (captureError) {
throw captureError;
}
function startFrameCapture(page) {
const startedAt = Date.now();
let index = 0;
let inFlight = Promise.resolve();
const timer = setInterval(() => {
const frameIndex = index;
index += 1;
inFlight = inFlight
.catch(() => {})
.then(async () => {
const elapsedMs = Date.now() - startedAt;
const summary = await readStateSummary(page);
const frameName = `frame-${String(frameIndex).padStart(5, "0")}-t${String(elapsedMs).padStart(6, "0")}ms-${slug(summary.runState)}.png`;
await page.screenshot({ path: resolve(outputDir, frameName), fullPage: true });
frames.push({
index: frameIndex,
elapsedMs,
file: frameName,
runState: summary.runState,
taskState: summary.taskState,
mode: summary.mode,
interpState: summary.interpState,
taskPaused: summary.taskPaused,
activeLine: summary.activeLine,
sampleIndex: summary.programExecutionSampleIndex,
sourceFile: summary.programUiExecution?.sourceFile || null,
line: summary.programUiExecution?.line || null,
statement: summary.programUiExecution?.statement || "",
currentVelocity: summary.currentVelocity,
});
});
}, samplePeriodMs);
return { timer, get inFlight() { return inFlight; } };
}
async function clickAndWait(page, selector, label, predicate, timeoutMs = 45000) {
const before = await readStateSummary(page);
const startedAt = Date.now();
await page.click(selector);
const after = await waitForState(page, predicate, label, timeoutMs);
const event = {
label,
selector,
at: new Date().toISOString(),
elapsedMs: Date.now() - startedAt,
before,
after: summarizeForEvent(after),
};
events.push(event);
assertions.push({
label,
passed: true,
checkedAt: new Date().toISOString(),
state: summarizeForEvent(after),
});
return after;
}
async function holdState(page, label, durationMs, predicate, options = {}) {
const startedAt = Date.now();
let last = null;
const frozen = options.freezePosition ? await readStateSummary(page) : null;
const motionBaseline = options.requirePositionChange ? await readStateSummary(page) : null;
let movedDuringHold = false;
while (Date.now() - startedAt < durationMs) {
const state = await readRawState(page);
last = state;
if (!predicate(state)) {
throw new Error(`${label} failed at ${Date.now() - startedAt}ms: ${JSON.stringify(summarizeForEvent(state))}`);
}
if (frozen) {
const live = await readStateSummary(page);
assertPositionFrozen(frozen, live, label, Date.now() - startedAt);
}
if (motionBaseline && !movedDuringHold) {
const live = await readStateSummary(page);
movedDuringHold = hasPositionChanged(motionBaseline, live);
}
await page.waitForTimeout(100);
}
if (motionBaseline && !movedDuringHold) {
throw new Error(`${label} failed: Position did not change while runState stayed running`);
}
const event = {
label,
at: new Date().toISOString(),
durationMs: Date.now() - startedAt,
state: summarizeForEvent(last),
frozenPosition: frozen ? {
axisPose: frozen.axisPose,
dro: frozen.dro,
canvasToolhead: frozen.canvasToolhead,
canvasVismachPins: frozen.canvasVismachPins,
} : null,
movedDuringHold: motionBaseline ? movedDuringHold : null,
};
events.push(event);
assertions.push({
label,
passed: true,
checkedAt: new Date().toISOString(),
state: summarizeForEvent(last),
});
return last;
}
async function snapshot(page, label) {
const summary = await readStateSummary(page);
const file = `snapshot-${slug(label)}.png`;
await page.screenshot({ path: resolve(outputDir, file), fullPage: true });
events.push({
label,
at: new Date().toISOString(),
snapshot: file,
state: summary,
});
}
async function ensureEstopActive(page) {
const state = await readRawState(page);
if (state.machine?.estopActive === true || state.machine?.taskState === "estop") return;
await page.evaluate(() => window.webRtcp5AxisSimulation.dispatch({ type: "ESTOP" }));
await waitForState(page, (nextState) => (
nextState.machine?.estopActive === true &&
nextState.machine?.taskState === "estop"
), "setup ESTOP active");
}
async function waitForState(page, predicate, label, timeoutMs = 30000) {
const predicateText = predicate.toString();
await page.waitForFunction(
([source, selectedSourceRel]) => {
const state = window.webRtcp5AxisSimulation?.getState?.();
if (!state) return false;
const sourceRel = selectedSourceRel;
return Function("state", "sourceRel", `return (${source})(state, sourceRel);`)(state, sourceRel);
},
[predicateText, sourceRel],
{ timeout: timeoutMs },
).catch(async (error) => {
const state = await readStateSummary(page).catch(() => null);
throw new Error(`Timed out waiting for ${label}: ${error.message}\n${JSON.stringify(state, null, 2)}`);
});
return readRawState(page);
}
async function readRawState(page) {
return page.evaluate(() => window.webRtcp5AxisSimulation.getState());
}
async function readStateSummary(page) {
return page.evaluate(() => {
const state = window.webRtcp5AxisSimulation.getState();
const pauseButton = document.querySelector('[data-tool-id="tbtn_pause"]');
const canvas = document.querySelector("[data-five-axis-canvas]");
const programPane = document.querySelector('[data-region="program"]');
const monitor = document.querySelector("[data-linuxcnc-process-monitor]");
const parseJsonDataset = (value) => {
try {
return value ? JSON.parse(value) : null;
} catch {
return null;
}
};
const canvasToolhead = parseJsonDataset(canvas?.dataset?.threeToolhead);
const canvasVismach = parseJsonDataset(canvas?.dataset?.threeVismachModel);
return {
machineProfile: state.machineProfile,
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null,
taskHalLoaded: Boolean(state.taskHalRuntime?.loaded),
taskHalRuntimeReadiness: state.taskHalRuntimeReadiness,
runState: state.runState,
taskState: state.machine?.taskState,
powerOn: state.machine?.powerOn,
estopActive: state.machine?.estopActive,
allHomed: state.machine?.allHomed,
mode: state.machine?.mode,
interpState: state.machine?.interpState,
interpResumeState: state.machine?.interpResumeState,
taskPaused: state.machine?.taskPaused,
rtcpState: state.rtcpState,
kinsType: state.kinsType,
activeLine: state.activeLine,
programExecutionSourceMode: state.programExecutionSourceMode,
programExecutionSampleIndex: state.programExecutionSampleIndex,
programRuntimeFeedback: state.programRuntimeFeedback,
axisPose: state.axisPose,
dro: state.dro,
programUiExecution: state.programUiExecution,
samplePeriodMs: state.programAxisPreviewPath?.samplePeriodMs || null,
sampleCount: state.programAxisPreviewPath?.sampleCount || state.programAxisPreviewPath?.samples?.length || 0,
gcodeExecutionProcess: state.programAxisPreviewPath?.gcodeExecutionProcess || null,
currentVelocity: state.feed?.currentVelocity || 0,
operatorMessage: state.operatorMessage,
pauseButton: pauseButton ? {
action: pauseButton.dataset.action || null,
paused: pauseButton.dataset.paused || null,
title: pauseButton.title || null,
ariaPressed: pauseButton.getAttribute("aria-pressed"),
} : null,
canvasDataset: canvas ? {
width: canvas.width,
height: canvas.height,
threeToolAxis: canvas.dataset.threeToolAxis || null,
threeToolGlyphAxis: canvas.dataset.threeToolGlyphAxis || null,
} : null,
canvasToolhead,
canvasVismachPins: canvasVismach?.pins || null,
programPaneDataset: programPane ? { ...programPane.dataset } : null,
processMonitorDataset: monitor ? { ...monitor.dataset } : null,
};
});
}
function assertPositionFrozen(expected, actual, label, elapsedMs) {
const checks = [
["axisPose", expected.axisPose, actual.axisPose, ["x", "y", "z", "b", "c"]],
["dro", expected.dro, actual.dro, ["x", "y", "z", "b", "c", "tcpX", "tcpY", "tcpZ"]],
["canvasToolhead", expected.canvasToolhead, actual.canvasToolhead, ["x", "y", "z"]],
["canvasVismachPins", expected.canvasVismachPins, actual.canvasVismachPins, ["table-x", "saddle-y", "spindle-z", "tilt-b", "rotate-c"]],
];
for (const [name, before, after, keys] of checks) {
if (!before || !after) continue;
for (const key of keys) {
const delta = Math.abs(Number(after[key] || 0) - Number(before[key] || 0));
if (delta > 1e-6) {
throw new Error(`${label} position changed at ${elapsedMs}ms: ${name}.${key} ${before[key]} -> ${after[key]}`);
}
}
}
}
function hasPositionChanged(before, after) {
const checks = [
[before.axisPose, after.axisPose, ["x", "y", "z", "b", "c"], 1e-6],
[before.dro, after.dro, ["x", "y", "z", "b", "c", "tcpX", "tcpY", "tcpZ"], 1e-6],
[before.canvasToolhead, after.canvasToolhead, ["x", "y", "z"], 1e-6],
[before.canvasVismachPins, after.canvasVismachPins, ["table-x", "saddle-y", "spindle-z", "tilt-b", "rotate-c"], 1e-6],
];
return checks.some(([left, right, keys, tolerance]) => (
left && right && keys.some((key) => Math.abs(Number(right[key] || 0) - Number(left[key] || 0)) > tolerance)
));
}
function summarizeForEvent(state) {
return {
runState: state?.runState,
taskState: state?.machine?.taskState || state?.taskState,
powerOn: state?.machine?.powerOn ?? state?.powerOn,
estopActive: state?.machine?.estopActive ?? state?.estopActive,
allHomed: state?.machine?.allHomed ?? state?.allHomed,
mode: state?.machine?.mode || state?.mode,
interpState: state?.machine?.interpState || state?.interpState,
taskPaused: state?.machine?.taskPaused ?? state?.taskPaused,
activeLine: state?.activeLine,
sampleIndex: state?.programExecutionSampleIndex,
programExecutionSourceMode: state?.programExecutionSourceMode,
programUiExecution: state?.programUiExecution,
currentVelocity: state?.feed?.currentVelocity ?? state?.currentVelocity,
operatorMessage: state?.operatorMessage,
};
}
async function assertCanvasHasPixels(page) {
const result = await page.evaluate(() => {
const canvas = document.querySelector("[data-five-axis-canvas]");
if (!canvas) return { ok: false, reason: "missing canvas" };
const width = canvas.width;
const height = canvas.height;
if (!width || !height) return { ok: false, reason: `invalid canvas size ${width}x${height}` };
const dataUrl = canvas.toDataURL("image/png");
return { ok: Boolean(dataUrl && dataUrl.length > 2000), width, height, dataUrlLength: dataUrl?.length || 0 };
});
if (!result.ok) {
throw new Error(`preview canvas not renderable: ${JSON.stringify(result)}`);
}
assertions.push({
label: "WebGL/canvas preview rendered",
passed: true,
checkedAt: new Date().toISOString(),
result,
});
}
function startStaticServer(root) {
const server = createServer((request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
const decoded = decodeURIComponent(url.pathname);
const relative = decoded === "/" ? "/index.html" : decoded;
const target = resolve(root, `.${relative}`);
if (!target.startsWith(root)) {
response.writeHead(403);
response.end("Forbidden");
return;
}
let itemStat;
try {
itemStat = statSync(target);
if (!itemStat.isFile()) throw new Error("not a file");
} catch {
response.writeHead(404);
response.end("Not found");
return;
}
response.writeHead(200, {
"content-type": contentType(target),
"content-length": itemStat.size,
"cache-control": "no-store",
});
createReadStream(target).pipe(response);
});
return new Promise((resolveStart, rejectStart) => {
server.on("error", rejectStart);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
resolveStart({
port: address.port,
close: () => new Promise((resolveClose) => server.close(resolveClose)),
});
});
});
}
function contentType(file) {
if (file.endsWith(".html")) return "text/html; charset=utf-8";
if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
if (file.endsWith(".mjs")) return "text/javascript; charset=utf-8";
if (file.endsWith(".css")) return "text/css; charset=utf-8";
if (file.endsWith(".json")) return "application/json; charset=utf-8";
if (file.endsWith(".wasm")) return "application/wasm";
if (file.endsWith(".svg")) return "image/svg+xml";
if (file.endsWith(".png")) return "image/png";
return "application/octet-stream";
}
function findSystemChromium() {
const candidates = [
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
];
for (const candidate of candidates) {
try {
statSync(candidate);
return candidate;
} catch {
// Continue probing common browser locations.
}
}
return null;
}
function slug(value) {
return String(value || "state")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 48) || "state";
}

View File

@@ -0,0 +1,20 @@
# 2026-07-06 xyzbc-trt 任务态按钮逻辑严格对标
## 用户提问
完全对标 `/home/mes123456/cnc_wams/linuxcnc``xyzbc-trt` 中解除 `ESTOP``上电``Home All``Run``暂停` 功能,修改 Web 仿真系统 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 的混乱逻辑,并进行严格验证。要求新任务先写入 `working` 文档后再执行。
## 执行目标
1. 以本地 LinuxCNC `xyzbc-trt` 配置、HAL/任务状态语义和现有验证证据为参考,梳理按钮状态机。
2. 修正 Web 仿真中 `ESTOP`、机器上电、全轴回零、程序运行、暂停/继续的状态流转和按钮可用性。
3. 增加或更新自动化验证,覆盖未解除急停、未上电、未回零、运行、暂停、继续等关键路径。
4. 执行构建/节点测试/浏览器验证等可用验证,并把结果记录到最终结论与日志。
## 执行顺序
1. 阅读 LinuxCNC `xyzbc-trt` 参考配置与 Web 当前任务态实现。
2. 定位混乱逻辑的根因,制定局部修改方案。
3. 修改源代码与必要测试。
4. 严格运行验证并保存证据。
5. 按仓库要求把完整执行过程追加到 `gptlog-process/gpdlog.md`

View File

@@ -0,0 +1,19 @@
# 2026-07-06 xyzbc-trt 暂停按钮严格对标
## 用户提问
严格对标“暂停”按钮的逻辑。一直就不好用。
## 执行目标
1. 对照本地 LinuxCNC AXIS 的 `task_pause``task_resume``task_pauseresume` 源码语义,明确暂停按钮应有行为。
2. 复查 Web 仿真中工具栏暂停按钮、菜单 Pause、菜单 Resume、底层 `PAUSE/RESUME/PAUSE_RESUME` 的分发与状态门禁。
3. 找出导致暂停按钮“不好用”的实际原因,修正为以 LinuxCNC 任务状态为准的稳定逻辑。
4. 使用节点测试、构建、浏览器烟测和真实按钮/位置冻结证据进行严格验证。
## 预期对标语义
1. 工具栏双竖线按钮对标 LinuxCNC AXIS `task_pauseresume`:运行中点击应暂停,暂停中点击应恢复。
2. 菜单 `Pause` 对标 `task_pause`:仅在 AUTO 且 interpreter 处于 reading/waiting 时暂停。
3. 菜单 `Resume` 对标 `task_resume`:仅在 paused 且 task mode 为 AUTO/MDI 时恢复。
4. 暂停后速度归零G-code 执行采样不继续推进DRO、axisPose、canvas toolhead 保持冻结。

View File

@@ -0,0 +1,55 @@
# 2026-07-06 AXIS 主控制按钮严格对标补齐
## 用户提问
对标 `doc/AXIS主控制按钮功能先决条件与状态影响详解.md`,修改项目 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan`,并依据 `working` 下既有任务文档实施。
## 本轮目标
1. 以本地 LinuxCNC AXIS `task_pause``task_resume``task_pauseresume` 源码和项目文档为准,复查主控制按钮状态门禁。
2. 收紧 Web 仿真中暂停相关门禁,避免仅凭 `runState` 的残留运行态绕过 AXIS 的 `task_mode``interp_state` 条件。
3. 更新自动化验证,覆盖菜单 Pause、工具栏 pause/resume 在 idle、manual、AUTO/MDI、paused 状态下的严格行为。
4. 重新构建 `dist`,执行节点 smoke、浏览器 smoke、Web evidence 与 compare。
## 实施结论
- `app/src/state/linuxcnc-task-policy.js`
- `canPause` 调整为仅在 `STATE_ON + MODE_AUTO + INTERP_READING/WAITING` 时为真。
- 菜单 `PAUSE` 严格对标 AXIS `task_pause`:非 AUTO 阻止,非 READING/WAITING 阻止。
- 工具栏 `PAUSE_RESUME` 的暂停分支对标 AXIS `task_pauseresume`:必须为 AUTO/MDI且解释器非 idle。
- `app/src/state/store.js`
- `PAUSE_RESUME` 不再因 `runState=running/stepping` 自动接管 manual 残留状态。
- `PAUSE_RESUME` 在 manual 模式直接返回 `pause blocked: task mode must be auto or MDI`
- `PAUSE_RESUME` 在 AUTO/MDI 且解释器 idle 时返回 `pause ignored: interpreter is idle`
- `tests/node/verify_xyzbc_trt_web_app.mjs`
- 移除旧的宽容断言idle interpreter + running runState 不应被菜单 Pause 暂停。
- 移除旧的宽容断言manual 残留 running 状态不应被工具栏 pause/resume 强制改成 AUTO paused。
- 增加对应严格拦截断言。
- `app/dist/src/...`
- 通过 `npm --prefix app run build``src` 重新生成。
## 验证结果
```text
npm --prefix app run smoke:node
xyzbc_trt_web_app_smoke=ok
npm --prefix app run build
gmoccapy_static_build=ok
npm --prefix app run smoke:browser
xyzbc_trt_browser_smoke=ok
npm --prefix app run evidence:web
web_xyzbc_trt_evidence=/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json
npm --prefix app run evidence:compare
compare_xyzbc_trt_status=pass
```
## 注意事项
本轮开始前工作区已有多处未提交修改和新增证据文件。本轮只在现有状态上补齐 AXIS 暂停/恢复门禁,不回退既有改动。

View File

@@ -0,0 +1,83 @@
# 2026-07-06 AXIS 主控制按钮完全对标复核
## 用户提问
项目 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan` 要完全对标 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/doc/AXIS主控制按钮功能先决条件与状态影响详解.md`
## 对标范围
按文档逐项复核:
- 急停与解除急停:`estop_clicked()``STATE_ESTOP` / `STATE_ESTOP_RESET`
- 上电/下电:`onoff_clicked()`,仅 `STATE_ESTOP_RESET` 分支上电,其余状态发送 `STATE_OFF`
- Home All`home_all_joints()` / `c.home(-1)`
- Run`task_run()` / `AUTO_RUN`,要求上电、已回零、程序打开。
- Pause`task_pause()` / `AUTO_PAUSE`,要求 AUTO 且解释器 READING/WAITING。
- Resume`task_resume()` / `AUTO_RESUME`,要求 paused 且 AUTO/MDI。
- Pause/Resume 工具栏合并按钮:`task_pauseresume()`,要求 AUTO/MDIpaused 时恢复,非 idle 时暂停。
- Step`task_step()` / `AUTO_STEP`,支持自动空闲初次单步,也支持 reading/waiting/paused 队列单步推进。
## 本轮发现和修正
### P-016-001 电源按钮 ESTOP/OFF 语义偏差
问题:
Web 原 `TOGGLE_POWER``STATE_ESTOP` 下会被策略阻止并提示先 reset在非 on 状态下容易按“切换”理解直接进入 on。LinuxCNC AXIS 的 `onoff_clicked()` 并不是“任意非 on 上电”,而是:
```python
if s.task_state == linuxcnc.STATE_ESTOP_RESET:
c.state(linuxcnc.STATE_ON)
else:
c.state(linuxcnc.STATE_OFF)
```
修正:
- `app/src/state/linuxcnc-task-policy.js`
- `TOGGLE_POWER` 不再在 `STATE_ESTOP` 下阻止,允许按钮动作进入 OFF 分支。
- `app/src/state/store.js`
- `TOGGLE_POWER` 改为 `taskState === "estop-reset"` 时上电,否则下电到 `taskState="off"`
- 下电时关闭 power、motion、RTCP、冷却、主轴速度并清除 `allHomed`,对标 `emcJointUnhome(-2)` 的 volatile home 清除效果。
- task/HAL 命令改为上电发送 `EMC_TASK_SET_STATE ON`,否则发送 `EMC_TASK_SET_STATE OFF`
### P-016-002 自动化验证补齐
- `tests/node/verify_xyzbc_trt_web_app.mjs`
- 新增 ESTOP 下点击 Power 进入 OFF 的断言。
- 新增 OFF 下继续点击 Power 仍保持 OFF不能绕过 reset 直接上电的断言。
- 新增下电清除 `allHomed` 的断言。
- 将主按钮链路测试和后续 MDI/JOG/override 测试拆分为独立 store避免异步运行状态污染。
- `tests/browser/xyzbc_trt_browser_smoke.html`
- 新增真实 DOM 按钮路径ESTOP 后点 Power 进入 OFFOFF 下点 Power 仍保持 OFF再 ESTOP/RESET 后才能上电。
## 验证结果
```text
npm --prefix app run smoke:node
xyzbc_trt_web_app_smoke=ok
npm --prefix app run build
gmoccapy_static_build=ok
npm --prefix app run smoke:browser
xyzbc_trt_browser_smoke=ok
npm --prefix app run evidence:web
web_xyzbc_trt_evidence=/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json
web status=ready-for-wasm-runtime
blockers=[]
npm --prefix app run evidence:compare
compare_xyzbc_trt_status=pass
checkCount=60
passCount=60
failCount=0
blockers=[]
```
## 结论
本轮按参考文档完成主控制按钮全量复核,并修正电源按钮与 AXIS `onoff_clicked()` 不一致的问题。当前 ESTOP、RESET、POWER、HOME、RUN、PAUSE、RESUME、PAUSE_RESUME、STEP 均有源码门禁和自动化验证覆盖Web/native evidence compare 为 60/60 pass。

View File

@@ -0,0 +1,115 @@
# 2026-07-06 暂停按钮严格测试报告
## 用户提问
严格测试“暂停”按钮的功能。
## 测试目标
`working/14-20260706-xyzbc-trt-暂停按钮严格对标.md` 的预期语义验证:
- 工具栏双竖线按钮对标 AXIS `task_pauseresume`:运行中暂停,暂停中恢复。
- 菜单 `Pause` 对标 `task_pause`:仅 AUTO 且 interpreter 为 reading/waiting 时暂停。
- 菜单 `Resume` 对标 `task_resume`paused 且 AUTO/MDI 时恢复。
- 暂停后速度归零G-code 采样不继续推进DRO、axisPose、canvas toolhead 保持冻结。
## 执行命令和结果
### 1. 构建与回归
```text
npm --prefix app run build
gmoccapy_static_build=ok
npm --prefix app run smoke:node
xyzbc_trt_web_app_smoke=ok
npm --prefix app run smoke:browser
xyzbc_trt_browser_smoke=ok
```
### 2. 暂停位置 JSON trace
命令:
```text
APP_URL_PATH=/web-rtcp-5axis-xyzbc-trt-sim-plan/app/dist/index.html node tools/trace-pause-position-json.mjs
```
结果:
```text
pause_position_status=passed-position-frozen
trace=/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/pause-position-traces/pause-position-20260706T063131Z/trace.json
position_changed=false
changed_fields=
```
关键证据:
```text
pausedSampleCount=35
baselineLabel=pause-start
sampleIndex=48
currentVelocity=0
changedFields=[]
```
暂停开始到最后一个暂停采样期间:
- `runState=paused`
- `taskState=on`
- `mode=auto`
- `interpState=paused`
- `taskPaused=true`
- `sampleIndex=48` 保持不变
- `currentVelocity=0`
- `axisPose``dro``runtimeAxisPose``runtimeTcp``uiExecution.joint``uiExecution.tcp``canvasToolhead` 均无变化
- 暂停按钮状态为 `paused=true`,标题为 `Resume program`
### 3. 50ms 长流程截图验证
命令:
```text
RUN_STABLE_BEFORE_FIRST_PAUSE_MS=3000 node tools/verify-estop-power-home-run-pause-50ms.mjs
```
结果:
```text
verification_status=passed
screenshots=/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T063233Z
captured_frames=403
sample_period_ms=50
manifest=/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260706T063233Z/manifest.json
```
关键断言:
| 阶段 | 结果 | runState | mode | interpState | taskPaused | sampleIndex | currentVelocity |
|---|---|---|---|---|---|---:|---:|
| 第一次暂停 | 通过 | paused | auto | paused | true | 111 | 0 |
| 第一次暂停保持 5 秒 | 通过 | paused | auto | paused | true | 111 | 0 |
| 第二次暂停 | 通过 | paused | auto | paused | true | 405 | 0 |
| 第二次暂停保持 5 秒 | 通过 | paused | auto | paused | true | 405 | 0 |
两次暂停都发生在真实展开程序 `helix_bc.ngc` 第 17 行:
```text
f#<frate> g2i#<r>z#<zmin> p#<n>
```
### 4. Web/native evidence
```text
npm --prefix app run evidence:web
web_xyzbc_trt_evidence=/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json
npm --prefix app run evidence:compare
compare_xyzbc_trt_status=pass
```
## 结论
暂停按钮严格测试通过。工具栏暂停/恢复、菜单 Pause/Resume 的回归测试通过;真实浏览器中暂停后状态稳定为 `paused`,速度为 0采样索引不推进DRO/axisPose/runtime/canvas toolhead 均保持冻结;两次 5 秒暂停保持均通过50ms 截图长流程验证通过Web/native compare 仍为 pass。

View File

@@ -18,6 +18,7 @@
当前新增关注点:
- 2026-07-06 00:22 EDT 新任务:完全对标 `/home/mes123456/cnc_wams/linuxcnc``xyzbc-trt` 的 AXIS/task 状态联锁,重整 Web 仿真系统 `解除 ESTOP``上电``Home All``Run``暂停/继续` 功能逻辑;要求修复当前混乱状态流,并通过源码对照、自动化 smoke、browser 验证和 evidence/compare 严格复验。
- 2026-07-05 18:57 EDT 已按 LinuxCNC AXIS 源码修复 Pause工具栏按钮恢复为 `.toolbar.program_pause -> task_pauseresume` 语义,运行中点击暂停、暂停中点击继续;菜单 Pause/Resume 仍为独立命令。Node smoke、browser smoke、build、native/Web/compare 均通过,最新 compare 为 `60/60 pass`
- 2026-07-05 18:18 EDT 已按 `working/12-20260704-真实执行复验与缺失功能工作计划.md` 的 P-002 规则再次执行全量复验native LinuxCNC、Web evidence、compare、build、Node smoke、browser smoke 均通过;最新 `compare-xyzbc-trt-evidence.json``60/60 pass``blockers=[]``requiredImprovements=[]`
- 2026-07-05 18:08 EDT 已按用户要求重新执行“除硬件相关外完全对标”复验native LinuxCNC、Web evidence、compare、build、Node smoke、browser smoke 均通过;最新 `compare-xyzbc-trt-evidence.json``60/60 pass``blockers=[]``requiredImprovements=[]`

View File

@@ -1,7 +1,7 @@
{
"apiName": "xyzbc-trt-native-web-evidence-comparison",
"status": "pass",
"comparedAt": "2026-07-05T22:57:05.625Z",
"comparedAt": "2026-07-06T06:35:08.413Z",
"nativePath": "/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/native-xyzbc-trt-evidence.json",
"webPath": "/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/web-xyzbc-trt-evidence.json",
"summary": {
@@ -973,8 +973,8 @@
"id": "menu-run-ready",
"action": "run-ready",
"sourceSymbol": "AXIS run preconditions",
"sourceLines": "axis.py:2308-2320",
"expected": "power on, home, auto mode and TCP kins ready"
"sourceLines": "axis.py:2379-2391",
"expected": "open/stage the selected program without substituting power, Home All, or Run"
},
{
"id": "menu-run",

View File

@@ -1,7 +1,7 @@
{
"apiName": "xyzbc-trt-web-opfs-wasm-evidence",
"status": "ready-for-wasm-runtime",
"collectedAt": "2026-07-05T22:56:54.459Z",
"collectedAt": "2026-07-06T06:35:07.906Z",
"profile": {
"id": "xyzbc-trt",
"machineName": "sim-xyzbc-trt-kins (switchkins)",
@@ -227889,8 +227889,8 @@
"id": "menu-run-ready",
"action": "run-ready",
"sourceSymbol": "AXIS run preconditions",
"sourceLines": "axis.py:2308-2320",
"expected": "power on, home, auto mode and TCP kins ready"
"sourceLines": "axis.py:2379-2391",
"expected": "open/stage the selected program without substituting power, Home All, or Run"
},
{
"id": "menu-run",
@@ -230061,8 +230061,8 @@
"id": "menu-run-ready",
"action": "run-ready",
"sourceSymbol": "AXIS run preconditions",
"sourceLines": "axis.py:2308-2320",
"expected": "power on, home, auto mode and TCP kins ready"
"sourceLines": "axis.py:2379-2391",
"expected": "open/stage the selected program without substituting power, Home All, or Run"
},
{
"id": "menu-run",

Some files were not shown because too many files have changed in this diff Show More