feat: complete working8 pause semantics

This commit is contained in:
wangdequan
2026-07-07 10:27:02 -04:00
parent 83402c506e
commit 4ba45b2ec5
987 changed files with 340641 additions and 65 deletions

View File

@@ -3109,3 +3109,97 @@ Pause 修复已提交并推送到云仓库 `master`,提交号为 `58854f9`。
### 结论 ### 结论
已完成 `/home/mes123456/cnc_wams/work/working8` 文档体系建设,新增 README 索引和 01 至 06 六个要求文档。文档明确了暂停按钮必须补齐 LinuxCNC task/interpreter 与 motion/trajectory 双层语义,拆分了 `W8-001``W8-014` 任务矩阵,并给出后续代码修改目标文件、验收标准和验证命令。本轮未修改业务代码,未运行测试。 已完成 `/home/mes123456/cnc_wams/work/working8` 文档体系建设,新增 README 索引和 01 至 06 六个要求文档。文档明确了暂停按钮必须补齐 LinuxCNC task/interpreter 与 motion/trajectory 双层语义,拆分了 `W8-001``W8-014` 任务矩阵,并给出后续代码修改目标文件、验收标准和验证命令。本轮未修改业务代码,未运行测试。
## 2026-07-07 09:41 EDT
### 提问
用户要求提交到云仓库,并提供云仓库地址 `http://154.8.160.151:3000`、用户名 `wangdequan`、密码 `wangdequan123`
### 执行过程
1. 查看 `git status --short` 和当前分支,确认当前分支为 `master`,工作区存在本轮 `work/working8` 文档、日志追加,以及此前已有的暂停按钮前端、文档、证据、截图等大量未提交改动。
2. 查看 `git remote -v`,确认 `origin` 已配置为 `http://wangdequan:wangdequan123@154.8.160.151:3000/wangdequan/cnc_wams.git`
3. 查看最近提交,确认当前本地历史最新远端提交为 `6cecd28 docs: record final artifact push`
4. 执行 `git fetch origin master`,成功连接云仓库并获取 `origin/master`
5. 执行 `git rev-list --left-right --count origin/master...HEAD`,输出 `0 0`,确认本地 `master` 与远端 `origin/master` 推送前没有分叉。
6. 统计未跟踪文件数量,确认未跟踪文件约 `3282` 个,主要是暂停验证 trace 和 50ms 截图证据目录;考虑用户要求提交云仓库,决定整体纳入提交,避免文档引用的证据缺失。
7. 执行 `git add -A`,暂存全部当前工作区改动。
8. 执行 `git commit -m "docs: record pause wasm implementation plan"`,成功创建提交 `83402c5`,提交包含 `3299` 个文件变更,包括 `work/working8` 文档体系、暂停按钮分析文档、暂停验证工具、暂停测试报告、trace、截图证据、日志和此前已有前端相关改动。
9. 执行 `git push origin master`,推送过程中等待远端处理,最终成功输出 `6cecd28..83402c5 master -> master`
10. 执行 `git status -sb && git rev-parse HEAD && git rev-parse origin/master`,确认本地 `HEAD``origin/master` 均为 `83402c506e26cfcae2ba567a8188ae858858b918`,本地与远端一致。
### 结论
已成功提交并推送到云仓库 `origin/master`。提交哈希为 `83402c506e26cfcae2ba567a8188ae858858b918`,提交信息为 `docs: record pause wasm implementation plan`。本次日志是在推送完成后按规则追加,因此日志文件本身会产生新的未推送工作区改动。
## 2026-07-07 10:16 EDT
### 提问
用户要求完成 `/home/mes123456/cnc_wams/work/working8` 的全部任务。
### 执行过程
1. 读取 `work/working8/README.md``01-项目功能内容.md``02-项目程序开发详细步骤.md``03-推进台账.md``04-任务矩阵.md``05-验收证据.md``06-决策记录.md`,确认本工作包目标是补齐 `wasm-port` 的 LinuxCNC task/motion/TP 暂停语义,并让 `web-rtcp-5axis-xyzbc-trt-sim-plan` 的 AXIS 风格暂停按钮具备真实底层暂停/恢复/单步行为。
2. 查看 `git status --short`,确认开始前已有 `gptlog-process/gpdlog.md``web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md` 未提交修改;本轮未回退这些既有修改。
3. 读取 `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp`,确认原实现中 `EMC_TASK_PLAN_PAUSE` 会设置 `interp_state=PAUSED``task_paused=true`,但缺少 `interpResumeState` 保存和输出,`RESUME` 固定近似恢复到 `READING`
4. 读取 `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c`,确认原 motion pause 仍作为普通队列命令处理,`STEP` 近似普通 resume缺少 motion id/idForStep/stepping 跟踪和“只推进一个 motion 后自动回暂停”语义。
5. 读取 `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c``wasm-port/tests/wasm/node/verify_tp_wasm.mjs`,确认已有 TP line/arc/queue probe但没有 `tpPause()/tpResume()` 专门验证。
6. 读取 Web 侧 `app/src/runtime/linuxcnc-task-hal-runtime.js``app/src/state/linuxcnc-task-policy.js``app/src/state/store.js``app/src/ui/axis-shell.js``tests/node/verify_run_feedback_loop.mjs`,确认前端 resume gate 主要依赖 `interpState === paused`,尚未完整接入 motion paused。
7. 修改 `linuxcnc_task_hal_wasm.cpp`:在 `TaskRuntime` 增加 `interp_resume_state`status JSON 输出 `task.interpResumeState`session init、open program、run、pause、step、resume、abort、MDI、jog、home、program complete 等路径同步维护该字段pause 时保存暂停前状态resume 时按保存状态恢复step 保持外部 `PAUSED`,仅在 `lctask_run_cycles()` 内部允许推进一条。
8. 修改 `linuxcnc_motion_runtime.c`:增加 `stepping``id_for_step``motion_id`;把 `EMCMOT/EMC_TRAJ` 的 pause/resume/step/abort 改成即时命令pause 设置 `paused=1`、速度归零、不清空队列paused 期间不消费普通 movestep 记录当前 motion id允许消费一个普通 move 后自动 `paused=1``stepping=0`status JSON 输出 `motion.stepping``motion.idForStep``motion.motionId``motion.commandQueueDepth` 和根级 `commandQueueDepth`
9. 修改 `linuxcnc_tp_wasm.c`:新增 pause/resume probe创建 TP、加入两段 line、运行后调用 vendored LinuxCNC `tpPause()`,等待速度归零并保持位置,再调用 `tpResume()` 验证队列继续完成;输出 `tp_pause_rc=0``tp_resume_rc=0``tp_paused_velocity_zero=1``tp_paused_position_frozen=1``tp_resume_done=1` 等字段。
10. 修改 `verify_task_hal_wasm.mjs`:扩展为四行程序运行中 pause/resume/step 验证;断言 pause 后 `interpState=PAUSED``interpResumeState=READING``taskPaused=true``motion.paused=true`、连续 cycles 不推进 task line/motion line/axis/motion idresume 后恢复读取并继续task STEP 后回到 paused直接调用 motion C ABI 验证 queued moves 在 pause 后 queue 保留、step 只消费一个、resume 后继续消费。
11. 修改 `verify_tp_wasm.mjs`:增加对 TP pause/resume probe 输出的断言,并输出 `tp_pause_resume_smoke=ok`
12. 修改 Web runtime normalize`normalizeTaskHalStatus()` 中映射 `ui.interpResumeState``ui.taskPaused``ui.singleStepping``ui.motionPaused``ui.motionStepping``ui.motionId``ui.idForStep``ui.motionQueueDepth`
13. 修改 `linuxcnc-task-policy.js`:新增 `motionPaused``taskPaused``feedbackPaused``paused` 综合状态;`canResume``RESUME` gate 接受 motion paused/task paused/interp paused/runState paused菜单 Pause 仍保持 AUTO 且 interpreter running 的严格语义。
14. 修改 `store.js`:初始 `machine` 增加 `motionPaused``PAUSE``RESUME``STEP`、task-hal status apply、normalizeMachine、runtime feedback、shouldContinueTaskHalStatusLoop 等路径同步维护和使用 motion paused`PAUSE_RESUME` 优先用 `machine.motionPaused` 判断是否触发 resume`programRuntimeFeedback` 增加 `paused` 字段。
15. 修改 `axis-shell.js`:暂停按钮 active/title/data-paused 判断接入 `machine.motionPaused``programRuntimeFeedback.paused`
16. 修改 `verify_run_feedback_loop.mjs`:新增 Web Node 暂停断言,点击 `PAUSE_RESUME` 后检查 `machine.motionPaused=true``taskHalStatus.ui.motionPaused=true``taskHalStatus.motionStatus.motion.paused=true``programRuntimeFeedback.paused=true`,并确认 active line、motion program line、motion axis 不推进;再次点击同一按钮恢复;同时修正 Run Ready 测试,使其只验证程序准备,然后显式执行 power/home/auto/run。
17. 修改 `verify-estop-power-home-run-pause-50ms.mjs`:在 screenshot frame、state summary 和 event summary 中加入 `motionPaused`,使 manifest 可记录两次暂停保持阶段的 `motionPaused=true`
18. 首次执行 `./tools/build_task_hal_wasm.sh` 失败,错误为当前 shell 找不到 `emcc`;查找发现 `/home/mes123456/emsdk/emsdk_env.sh``/home/mes123456/emsdk/upstream/emscripten/emcc`
19. 执行 `source /home/mes123456/emsdk/emsdk_env.sh >/dev/null && ./tools/build_task_hal_wasm.sh`,构建通过,输出 `linuxcnc_task_hal_wasm_build=ok`
20. 执行 `node tests/wasm/node/verify_task_hal_wasm.mjs`,通过,输出 `linuxcnc_task_runtime_smoke=ok``task_status_from_linuxcnc_runtime=ok``task_commands_drive_motion_runtime=ok``mdi_jog_task_motion_hal_sync=ok``pause_freezes_motion_queue=ok``resume_restores_interp_resume_state=ok``step_returns_to_paused=ok`
21. 执行 `source /home/mes123456/emsdk/emsdk_env.sh >/dev/null && ./tools/build_tp_wasm.sh`TP WASM 构建完成。
22. 执行 `node tests/wasm/node/verify_tp_wasm.mjs`,通过,输出 `tp_wasm_node_smoke=ok``tp_pause_resume_smoke=ok`
23. 执行 `npm --prefix app run build`,通过,输出 `gmoccapy_static_build=ok`,并更新 `app/dist` 中静态产物和 WASM artifact。
24. 执行 `node tests/node/verify_run_feedback_loop.mjs`,第一次失败在新增暂停断言中直接比较 UI `axisPose`,发现该字段会受异步 RTCP/kinematics 刷新影响而符号变化;将断言改为比较底层 `taskHalStatus.motionStatus.axis`
25. 再次执行 `node tests/node/verify_run_feedback_loop.mjs`,失败在既有 Run Ready 等待条件;确认 Run Ready 按 AXIS parity 应只准备程序,不应代替 power/home/run修改测试为 Run Ready 后显式执行 `TOGGLE_POWER``HOME``SET_MODE auto``RUN`
26. 再次执行 `node tests/node/verify_run_feedback_loop.mjs`,通过,输出 `run_feedback_status_loop_smoke=ok``run_ready_sequence_smoke=ok``pause_uses_motion_paused_gate=ok``pause_freezes_task_hal_status_loop=ok`
27. 执行 `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``position_changed=false``changed_fields=`trace 文件为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/pause-position-traces/pause-position-20260707T140646Z/trace.json`
28. 执行 `RUN_STABLE_BEFORE_FIRST_PAUSE_MS=3000 node tools/verify-estop-power-home-run-pause-50ms.mjs`,首次通过并生成截图目录 `estop-power-home-run-pause-50ms-20260707T140708Z`,随后发现 manifest 缺少 `motionPaused` 字段,因此补工具后重跑。
29. 重新执行 `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-20260707T141219Z`manifest 为 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260707T141219Z/manifest.json`,采集 `459` 帧,采样周期 `50ms`
30. 使用 `jq` 提取新 manifest 暂停保持证据:第一次暂停保持 5 秒为 `sampleIndex=109``activeLine=17``interpState=paused``taskPaused=true``motionPaused=true``currentVelocity=0`;第二次暂停保持 5 秒为 `sampleIndex=341``activeLine=13``interpState=paused``taskPaused=true``motionPaused=true``currentVelocity=0`
31. 更新 `work/working8/04-任务矩阵.md`,将 `W8-001``W8-014` 全部标记为 `已完成`
32. 更新 `work/working8/05-验收证据.md`,追加本轮 WASM task/motion、TP、Web Node、浏览器 trace、50ms 长流程截图验证的命令、关键输出、生成文件路径和结论。
33. 更新 `work/working8/03-推进台账.md`,追加 2026-07-07 轮次 2 的用户目标、本轮实现、修改文件、验证结果、证据路径、遗留问题和下一步。
34. 更新 `work/working8/06-决策记录.md`,新增 `DR-W8-007`,明确 Run Ready 只准备程序,不代替 ESTOP/Power/Home/Run避免污染暂停按钮验收。
35. 执行 `git diff --check`,通过,无空白错误。
36. 执行 `git status --short``git diff --stat`,确认本轮修改覆盖 wasm-port runtime/test、Web runtime/policy/store/UI/test/tool、working8 文档、app/dist 构建产物,以及本轮新增 trace 和截图证据目录;同时保留开始前已存在的其他日志修改。
37. 按根目录 AGENTS.md 要求,将本轮完整中文执行过程追加到 `gptlog-process/gpdlog.md`
### 结论
`work/working8``W8-001``W8-014` 已全部完成。底层 task/motion 暂停恢复和 STEP 可观测语义已补齐TP pause/resume probe 已验证 vendored LinuxCNC APIWeb task policy/store/UI 已接入 motion pausedNode/WASM/Web/browser trace/50ms 长流程截图验收全部通过。主要证据路径为 trace `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/pause-position-traces/pause-position-20260707T140646Z/trace.json` 和 50ms manifest `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots/estop-power-home-run-pause-50ms-20260707T141219Z/manifest.json`
## 2026-07-07 10:25 EDT
### 提问
用户要求提交到云仓库,并提供云仓库地址 `http://154.8.160.151:3000`、用户名 `wangdequan`、密码 `wangdequan123`
### 执行过程
1. 查看 `git status --short` 和当前分支,确认当前分支为 `master`,工作区包含本轮 `working8` 实现、验证证据、构建产物、过程日志,以及一个既有修改文件 `备忘/16备用暂停按钮实现详细过程.txt`
2. 查看 `git remote -v`,确认 `origin` 已配置为 `http://wangdequan:wangdequan123@154.8.160.151:3000/wangdequan/cnc_wams.git`,与用户提供的云仓库地址和账号信息一致。
3. 查看最近提交,确认当前最新提交为 `83402c5 docs: record pause wasm implementation plan`
4. 执行 `git fetch origin master`,成功从云仓库获取 `origin/master`
5. 执行 `git rev-list --left-right --count origin/master...HEAD`,输出 `0 0`,确认本地 `master` 与远端 `origin/master` 没有分叉。
6. 执行 `git diff --stat`,确认待提交内容覆盖 wasm-port task/motion/TP 暂停语义、WASM/TP/Web 测试、Web runtime/policy/store/UI、working8 文档、trace/截图证据、构建产物和日志。
7. 执行 `git diff --check`,通过,无空白错误。
8. 按项目日志规则,先将本次云仓库提交过程中文日志追加到 `gptlog-process/gpdlog.md`,随后将执行 `git add -A``git commit``git push origin master`,确保日志也随本次提交进入云仓库。
### 结论
本次云仓库提交前检查通过:远端可访问,本地与远端无分叉,工作区改动可提交,空白检查通过。下一步将提交全部当前改动并推送到 `origin/master`

View File

@@ -39,6 +39,9 @@ typedef struct {
int teleop_mode; int teleop_mode;
int in_position; int in_position;
int paused; int paused;
int stepping;
int id_for_step;
int motion_id;
int aborted; int aborted;
int switchkins_type; int switchkins_type;
double requested_vel; double requested_vel;
@@ -187,6 +190,13 @@ static int queue_pop(LcmotCommand *command)
return 1; return 1;
} }
static int is_motion_command(int type)
{
return type == LCMOT_CMD_LINEAR_MOVE ||
type == LCMOT_CMD_CIRCULAR_MOVE ||
type == LCMOT_CMD_JOG_INCR;
}
static void sync_hal_pins(void) static void sync_hal_pins(void)
{ {
int i; int i;
@@ -263,19 +273,29 @@ static void apply_command(const LcmotCommand *command)
switch (command->type) { switch (command->type) {
case LCMOT_CMD_PAUSE: case LCMOT_CMD_PAUSE:
state->paused = 1; state->paused = 1;
state->stepping = 0;
state->id_for_step = state->motion_id;
state->motion_type = 0; state->motion_type = 0;
state->current_vel = 0.0;
state->in_position = 0; state->in_position = 0;
break; break;
case LCMOT_CMD_STEP: case LCMOT_CMD_STEP:
state->paused = 0; state->stepping = 1;
state->id_for_step = state->motion_id;
state->paused = 1;
break; break;
case LCMOT_CMD_RESUME: case LCMOT_CMD_RESUME:
state->paused = 0; state->paused = 0;
state->stepping = 0;
state->id_for_step = state->motion_id;
break; break;
case LCMOT_CMD_ABORT: case LCMOT_CMD_ABORT:
state->aborted = 1; state->aborted = 1;
state->paused = 0; state->paused = 0;
state->stepping = 0;
state->id_for_step = state->motion_id;
state->motion_type = 0; state->motion_type = 0;
state->current_vel = 0.0;
state->in_position = 1; state->in_position = 1;
state->queue_head = 0; state->queue_head = 0;
state->queue_tail = 0; state->queue_tail = 0;
@@ -287,6 +307,7 @@ static void apply_command(const LcmotCommand *command)
break; break;
case LCMOT_CMD_LINEAR_MOVE: case LCMOT_CMD_LINEAR_MOVE:
case LCMOT_CMD_CIRCULAR_MOVE: case LCMOT_CMD_CIRCULAR_MOVE:
state->motion_id += 1;
state->program_line = command->line; state->program_line = command->line;
state->motion_type = command->type == LCMOT_CMD_CIRCULAR_MOVE ? 2 : 1; state->motion_type = command->type == LCMOT_CMD_CIRCULAR_MOVE ? 2 : 1;
state->coord_mode = 1; state->coord_mode = 1;
@@ -302,8 +323,14 @@ static void apply_command(const LcmotCommand *command)
state->current_vel = state->requested_vel; state->current_vel = state->requested_vel;
state->in_position = 1; state->in_position = 1;
state->motion_type = 0; state->motion_type = 0;
if (state->stepping) {
state->paused = 1;
state->stepping = 0;
state->current_vel = 0.0;
}
break; break;
case LCMOT_CMD_JOG_INCR: case LCMOT_CMD_JOG_INCR:
state->motion_id += 1;
i = command->axis_index; i = command->axis_index;
if (i < 0 || i >= 9) { if (i < 0 || i >= 9) {
i = 0; i = 0;
@@ -320,6 +347,11 @@ static void apply_command(const LcmotCommand *command)
state->current_vel = state->requested_vel; state->current_vel = state->requested_vel;
state->in_position = 1; state->in_position = 1;
state->motion_type = 0; state->motion_type = 0;
if (state->stepping) {
state->paused = 1;
state->stepping = 0;
state->current_vel = 0.0;
}
break; break;
default: default:
break; break;
@@ -381,6 +413,14 @@ int lcmot_write_command_json(const char *json)
} else { } else {
return -1; return -1;
} }
if (command.type == LCMOT_CMD_PAUSE ||
command.type == LCMOT_CMD_STEP ||
command.type == LCMOT_CMD_RESUME ||
command.type == LCMOT_CMD_ABORT) {
apply_command(&command);
sync_hal_pins();
return 0;
}
return queue_push(command); return queue_push(command);
} }
@@ -396,6 +436,7 @@ int lcmot_step_servo(long period_ns, int cycles)
if (!lcmot_state.aborted && lcmot_state.queue_count > 0) { if (!lcmot_state.aborted && lcmot_state.queue_count > 0) {
command = lcmot_state.queue[lcmot_state.queue_head]; command = lcmot_state.queue[lcmot_state.queue_head];
if (!lcmot_state.paused || if (!lcmot_state.paused ||
(lcmot_state.stepping && is_motion_command(command.type)) ||
command.type == LCMOT_CMD_STEP || command.type == LCMOT_CMD_STEP ||
command.type == LCMOT_CMD_RESUME || command.type == LCMOT_CMD_RESUME ||
command.type == LCMOT_CMD_ABORT) { command.type == LCMOT_CMD_ABORT) {
@@ -420,7 +461,9 @@ int lcmot_read_status_json(char *out, int out_len)
"\"cycle\":%lld," "\"cycle\":%lld,"
"\"motion\":{\"programLine\":%d,\"motionType\":%d,\"coordMode\":%d," "\"motion\":{\"programLine\":%d,\"motionType\":%d,\"coordMode\":%d,"
"\"teleopMode\":%d,\"inPosition\":%s,\"paused\":%s,\"aborted\":%s," "\"teleopMode\":%d,\"inPosition\":%s,\"paused\":%s,\"aborted\":%s,"
"\"switchkinsType\":%d,\"requestedVel\":%.17g,\"currentVel\":%.17g}," "\"stepping\":%s,\"idForStep\":%d,\"motionId\":%d,"
"\"switchkinsType\":%d,\"requestedVel\":%.17g,\"currentVel\":%.17g,"
"\"commandQueueDepth\":%d},"
"\"axis\":{\"x\":%.17g,\"y\":%.17g,\"z\":%.17g,\"a\":%.17g,\"b\":%.17g,\"c\":%.17g}," "\"axis\":{\"x\":%.17g,\"y\":%.17g,\"z\":%.17g,\"a\":%.17g,\"b\":%.17g,\"c\":%.17g},"
"\"joint0\":{\"motorPosCmd\":%.17g,\"motorPosFb\":%.17g}," "\"joint0\":{\"motorPosCmd\":%.17g,\"motorPosFb\":%.17g},"
"\"commandQueueDepth\":%d}", "\"commandQueueDepth\":%d}",
@@ -432,9 +475,13 @@ int lcmot_read_status_json(char *out, int out_len)
state->in_position ? "true" : "false", state->in_position ? "true" : "false",
state->paused ? "true" : "false", state->paused ? "true" : "false",
state->aborted ? "true" : "false", state->aborted ? "true" : "false",
state->stepping ? "true" : "false",
state->id_for_step,
state->motion_id,
state->switchkins_type, state->switchkins_type,
state->requested_vel, state->requested_vel,
state->current_vel, state->current_vel,
state->queue_count,
state->axis_fb[0], state->axis_fb[0],
state->axis_fb[1], state->axis_fb[1],
state->axis_fb[2], state->axis_fb[2],

View File

@@ -31,6 +31,7 @@ struct TaskRuntime {
std::string state = "ESTOP"; std::string state = "ESTOP";
std::string mode = "MANUAL"; std::string mode = "MANUAL";
std::string interp_state = "IDLE"; std::string interp_state = "IDLE";
std::string interp_resume_state = "IDLE";
std::string exec_state = "DONE"; std::string exec_state = "DONE";
bool task_paused = false; bool task_paused = false;
bool single_stepping = false; bool single_stepping = false;
@@ -394,7 +395,10 @@ void forward_timed_motion_sample(TaskRuntime &state)
if (state.active_segment_index >= static_cast<int>(state.motion_plan.size())) { if (state.active_segment_index >= static_cast<int>(state.motion_plan.size())) {
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "DONE"; state.exec_state = "DONE";
state.task_paused = false;
state.single_stepping = false;
state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count); state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count);
return; return;
} }
@@ -416,7 +420,10 @@ void forward_timed_motion_sample(TaskRuntime &state)
state.next_program_line = std::min(std::max(segment.line, 1), std::max(max_line, 1)); state.next_program_line = std::min(std::max(segment.line, 1), std::max(max_line, 1));
if (state.active_segment_index == static_cast<int>(state.motion_plan.size()) - 1 && progress >= 1.0) { if (state.active_segment_index == static_cast<int>(state.motion_plan.size()) - 1 && progress >= 1.0) {
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "DONE"; state.exec_state = "DONE";
state.task_paused = false;
state.single_stepping = false;
state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count); state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count);
state.events.push_back("task_motion_plan_complete"); state.events.push_back("task_motion_plan_complete");
} }
@@ -479,6 +486,7 @@ std::string status_json()
out << ",\"task\":{\"state\":\"" << json_escape(state.state) << "\""; out << ",\"task\":{\"state\":\"" << json_escape(state.state) << "\"";
out << ",\"mode\":\"" << json_escape(state.mode) << "\""; out << ",\"mode\":\"" << json_escape(state.mode) << "\"";
out << ",\"interpState\":\"" << json_escape(state.interp_state) << "\""; out << ",\"interpState\":\"" << json_escape(state.interp_state) << "\"";
out << ",\"interpResumeState\":\"" << json_escape(state.interp_resume_state) << "\"";
out << ",\"execState\":\"" << json_escape(state.exec_state) << "\""; out << ",\"execState\":\"" << json_escape(state.exec_state) << "\"";
out << ",\"taskPaused\":" << (state.task_paused ? "true" : "false"); out << ",\"taskPaused\":" << (state.task_paused ? "true" : "false");
out << ",\"singleStepping\":" << (state.single_stepping ? "true" : "false"); out << ",\"singleStepping\":" << (state.single_stepping ? "true" : "false");
@@ -527,6 +535,7 @@ int lctask_init_session(const char *session_json)
state.state = "ESTOP_RESET"; state.state = "ESTOP_RESET";
state.mode = "MANUAL"; state.mode = "MANUAL";
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "DONE"; state.exec_state = "DONE";
state.events.push_back("task_session_init"); state.events.push_back("task_session_init");
@@ -573,6 +582,7 @@ int lctask_open_program(const char *path)
state.active_segment_index = 0; state.active_segment_index = 0;
state.run_elapsed_seconds = 0.0; state.run_elapsed_seconds = 0.0;
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "DONE"; state.exec_state = "DONE";
state.events.push_back(std::string("task_open_program:") + path); state.events.push_back(std::string("task_open_program:") + path);
return 0; return 0;
@@ -617,6 +627,7 @@ int lctask_send_command_json(const char *command_json)
state.active_segment_index = 0; state.active_segment_index = 0;
state.run_elapsed_seconds = 0.0; state.run_elapsed_seconds = 0.0;
state.interp_state = "READING"; state.interp_state = "READING";
state.interp_resume_state = "READING";
state.exec_state = "WAITING_FOR_MOTION"; state.exec_state = "WAITING_FOR_MOTION";
state.task_paused = false; state.task_paused = false;
state.single_stepping = false; state.single_stepping = false;
@@ -624,6 +635,9 @@ int lctask_send_command_json(const char *command_json)
return 0; return 0;
} }
if (contains_token(command_json, "EMC_TASK_PLAN_PAUSE")) { if (contains_token(command_json, "EMC_TASK_PLAN_PAUSE")) {
if (state.interp_state != "PAUSED") {
state.interp_resume_state = state.interp_state.empty() ? "READING" : state.interp_state;
}
state.interp_state = "PAUSED"; state.interp_state = "PAUSED";
state.exec_state = "PAUSED"; state.exec_state = "PAUSED";
state.task_paused = true; state.task_paused = true;
@@ -631,19 +645,24 @@ int lctask_send_command_json(const char *command_json)
return forward_motion_command("{\"type\":\"EMC_TRAJ_PAUSE\"}"); return forward_motion_command("{\"type\":\"EMC_TRAJ_PAUSE\"}");
} }
if (contains_token(command_json, "EMC_TASK_PLAN_STEP")) { if (contains_token(command_json, "EMC_TASK_PLAN_STEP")) {
if (state.interp_state != "PAUSED") {
state.interp_resume_state = state.interp_state.empty() ? "READING" : state.interp_state;
}
state.single_stepping = true; state.single_stepping = true;
state.task_paused = true; state.task_paused = true;
if (state.interp_state == "PAUSED") { state.interp_state = "PAUSED";
state.interp_state = "READING"; state.exec_state = "PAUSED";
}
state.exec_state = "WAITING_FOR_MOTION";
state.events.push_back("task_plan_step"); state.events.push_back("task_plan_step");
forward_motion_command("{\"type\":\"EMC_TRAJ_STEP\"}"); forward_motion_command("{\"type\":\"EMC_TRAJ_STEP\"}");
return 0; return 0;
} }
if (contains_token(command_json, "EMC_TASK_PLAN_RESUME")) { if (contains_token(command_json, "EMC_TASK_PLAN_RESUME")) {
state.interp_state = "READING"; const std::string resume = state.interp_resume_state == "PAUSED" || state.interp_resume_state.empty()
state.exec_state = "WAITING_FOR_MOTION"; ? "READING"
: state.interp_resume_state;
state.interp_state = resume;
state.interp_resume_state = resume;
state.exec_state = resume == "IDLE" ? "DONE" : "WAITING_FOR_MOTION";
state.task_paused = false; state.task_paused = false;
state.single_stepping = false; state.single_stepping = false;
state.events.push_back("task_plan_resume"); state.events.push_back("task_plan_resume");
@@ -651,6 +670,7 @@ int lctask_send_command_json(const char *command_json)
} }
if (contains_token(command_json, "EMC_TASK_ABORT")) { if (contains_token(command_json, "EMC_TASK_ABORT")) {
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "DONE"; state.exec_state = "DONE";
state.task_paused = false; state.task_paused = false;
state.single_stepping = false; state.single_stepping = false;
@@ -661,6 +681,7 @@ int lctask_send_command_json(const char *command_json)
if (contains_token(command_json, "EMC_TASK_PLAN_EXECUTE")) { if (contains_token(command_json, "EMC_TASK_PLAN_EXECUTE")) {
state.mode = "MDI"; state.mode = "MDI";
state.interp_state = "READING"; state.interp_state = "READING";
state.interp_resume_state = "READING";
state.exec_state = "WAITING_FOR_MOTION"; state.exec_state = "WAITING_FOR_MOTION";
state.task_paused = false; state.task_paused = false;
enqueue_mdi(state, command_json); enqueue_mdi(state, command_json);
@@ -669,6 +690,7 @@ int lctask_send_command_json(const char *command_json)
if (contains_token(command_json, "EMC_JOG_INCR")) { if (contains_token(command_json, "EMC_JOG_INCR")) {
state.mode = "MANUAL"; state.mode = "MANUAL";
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "WAITING_FOR_MOTION"; state.exec_state = "WAITING_FOR_MOTION";
state.task_paused = false; state.task_paused = false;
state.events.push_back("task_jog_incr"); state.events.push_back("task_jog_incr");
@@ -677,6 +699,7 @@ int lctask_send_command_json(const char *command_json)
if (contains_token(command_json, "EMC_JOINT_HOME")) { if (contains_token(command_json, "EMC_JOINT_HOME")) {
state.mode = "MANUAL"; state.mode = "MANUAL";
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "DONE"; state.exec_state = "DONE";
state.task_paused = false; state.task_paused = false;
state.single_stepping = false; state.single_stepping = false;
@@ -708,7 +731,8 @@ int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles
const bool has_program_work = state.motion_plan_loaded const bool has_program_work = state.motion_plan_loaded
? state.active_segment_index < static_cast<int>(state.motion_plan.size()) ? state.active_segment_index < static_cast<int>(state.motion_plan.size())
: state.next_program_line < state.executable_line_count; : state.next_program_line < state.executable_line_count;
if (state.interp_state == "READING" && has_program_work) { const bool stepping = state.single_stepping;
if ((state.interp_state == "READING" || stepping) && has_program_work) {
if (state.motion_plan_loaded) { if (state.motion_plan_loaded) {
const double delta_seconds = task_period_ns > 0 ? static_cast<double>(task_period_ns) / 1000000000.0 : 0.01; const double delta_seconds = task_period_ns > 0 ? static_cast<double>(task_period_ns) / 1000000000.0 : 0.01;
state.run_elapsed_seconds += delta_seconds; state.run_elapsed_seconds += delta_seconds;
@@ -716,18 +740,24 @@ int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles
} else { } else {
enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]); enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
} }
if (!state.motion_plan_loaded && state.next_program_line >= state.executable_line_count) { if (stepping) {
state.interp_state = "PAUSED";
state.exec_state = "PAUSED";
state.task_paused = true;
state.single_stepping = false;
} else if (!state.motion_plan_loaded && state.next_program_line >= state.executable_line_count) {
state.interp_state = "IDLE"; state.interp_state = "IDLE";
state.interp_resume_state = "IDLE";
state.exec_state = "DONE"; state.exec_state = "DONE";
state.task_paused = false; state.task_paused = false;
state.single_stepping = false; state.single_stepping = false;
state.events.push_back("task_plan_complete"); state.events.push_back("task_plan_complete");
} }
if (state.single_stepping) { } else if (stepping) {
state.interp_state = "PAUSED"; state.interp_state = "PAUSED";
state.exec_state = "PAUSED"; state.exec_state = "PAUSED";
state.task_paused = true; state.task_paused = true;
} state.single_stepping = false;
} }
if (lcmot_step_servo(servo_period_ns, servo_per_task) != 0) { if (lcmot_step_servo(servo_period_ns, servo_per_task) != 0) {
return -1; return -1;

View File

@@ -1048,6 +1048,158 @@ static void append_queued_lines_probe(char *out, size_t size, size_t *offset)
append(out, size, offset, line); append(out, size, offset, line);
} }
static void append_pause_resume_probe(char *out, size_t size, size_t *offset)
{
init_motion_state();
TP_STRUCT tp;
EmcPose start;
EmcPose end1;
EmcPose end2;
struct state_tag_t tag;
memset(&tp, 0, sizeof(tp));
memset(&start, 0, sizeof(start));
memset(&end1, 0, sizeof(end1));
memset(&end2, 0, sizeof(end2));
memset(&tag, 0, sizeof(tag));
end1.tran.x = 1.0;
end2.tran.x = 2.0;
const int create_rc = tpCreate(&tp, TP_DEFAULT_QUEUE_SIZE, 4);
const int set_cycle_rc = tpSetCycleTime(&tp, 0.001);
const int set_pos_rc = tpSetPos(&tp, &start);
const int set_vmax_rc = tpSetVmax(&tp, 1.0, 1.0);
const int set_vlimit_rc = tpSetVlimit(&tp, 1.0);
const int set_amax_rc = tpSetAmax(&tp, 10.0);
const int set_term_rc = tpSetTermCond(&tp, TC_TERM_COND_STOP, 0.0);
const int add_line1_rc = tpAddLine(
&tp,
end1,
EMC_MOTION_TYPE_FEED,
1.0,
1.0,
10.0,
100.0,
status.enables_new,
0,
-1,
tag);
const int add_line2_rc = tpAddLine(
&tp,
end2,
EMC_MOTION_TYPE_FEED,
1.0,
1.0,
10.0,
100.0,
status.enables_new,
0,
-1,
tag);
int cycle_rc = 0;
int cycles_before_pause = 0;
for (; cycles_before_pause < 200 && !tpIsDone(&tp); ++cycles_before_pause) {
cycle_rc = tpRunCycle(&tp, 1000000);
if (cycle_rc < 0) {
break;
}
}
const int pause_rc = tpPause(&tp);
int pause_cycles = 0;
for (; pause_cycles < 10000; ++pause_cycles) {
cycle_rc = tpRunCycle(&tp, 1000000);
if (cycle_rc < 0 || fabs(status.current_vel) < 1e-9) {
break;
}
}
EmcPose paused_pos;
EmcPose hold_pos;
EmcPose final_pos;
memset(&paused_pos, 0, sizeof(paused_pos));
memset(&hold_pos, 0, sizeof(hold_pos));
memset(&final_pos, 0, sizeof(final_pos));
const int pause_get_pos_rc = tpGetPos(&tp, &paused_pos);
for (int hold = 0; hold < 100; ++hold) {
cycle_rc = tpRunCycle(&tp, 1000000);
if (cycle_rc < 0) {
break;
}
}
const int hold_get_pos_rc = tpGetPos(&tp, &hold_pos);
const int paused_velocity_zero = fabs(status.current_vel) < 1e-9 ? 1 : 0;
const int paused_position_frozen =
fabs(paused_pos.tran.x - hold_pos.tran.x) < 1e-6 &&
fabs(paused_pos.tran.y - hold_pos.tran.y) < 1e-6 &&
fabs(paused_pos.tran.z - hold_pos.tran.z) < 1e-6;
const int resume_rc = tpResume(&tp);
int resume_cycles = 0;
for (; resume_cycles < 30000 && !tpIsDone(&tp); ++resume_cycles) {
cycle_rc = tpRunCycle(&tp, 1000000);
if (cycle_rc < 0) {
break;
}
}
const int final_get_pos_rc = tpGetPos(&tp, &final_pos);
const int near_end = pose_near(&final_pos, &end2);
char line[1536];
snprintf(line, sizeof(line),
"tp_pause_create=%d\n"
"tp_pause_set_cycle_time=%d\n"
"tp_pause_set_pos=%d\n"
"tp_pause_set_vmax=%d\n"
"tp_pause_set_vlimit=%d\n"
"tp_pause_set_amax=%d\n"
"tp_pause_set_term_cond=%d\n"
"tp_pause_add_line1=%d\n"
"tp_pause_add_line2=%d\n"
"tp_pause_rc=%d\n"
"tp_pause_cycles_before=%d\n"
"tp_pause_decel_cycles=%d\n"
"tp_pause_get_pos=%d\n"
"tp_pause_hold_get_pos=%d\n"
"tp_paused_velocity_zero=%d\n"
"tp_paused_position_frozen=%d\n"
"tp_resume_rc=%d\n"
"tp_resume_cycles=%d\n"
"tp_resume_get_pos=%d\n"
"tp_resume_done=%d\n"
"tp_pause_resume_final_depth=%d\n"
"tp_pause_resume_final_pos_near_end=%d\n"
"tp_pause_resume_final_pos=%.12g,%.12g,%.12g\n",
create_rc,
set_cycle_rc,
set_pos_rc,
set_vmax_rc,
set_vlimit_rc,
set_amax_rc,
set_term_rc,
add_line1_rc,
add_line2_rc,
pause_rc,
cycles_before_pause,
pause_cycles,
pause_get_pos_rc,
hold_get_pos_rc,
paused_velocity_zero,
paused_position_frozen,
resume_rc,
resume_cycles,
final_get_pos_rc,
tpIsDone(&tp),
tpQueueDepth(&tp),
near_end,
final_pos.tran.x,
final_pos.tran.y,
final_pos.tran.z);
append(out, size, offset, line);
}
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
char *lctp_run_probe(void) char *lctp_run_probe(void)
{ {
@@ -1091,6 +1243,7 @@ char *lctp_run_probe(void)
append_linear_probe(out, 8192, &offset); append_linear_probe(out, 8192, &offset);
append_arc_probe(out, 8192, &offset); append_arc_probe(out, 8192, &offset);
append_queued_lines_probe(out, 8192, &offset); append_queued_lines_probe(out, 8192, &offset);
append_pause_resume_probe(out, 8192, &offset);
return out; return out;
} }

View File

@@ -52,10 +52,20 @@ function send(command) {
); );
} }
function sendMotion(command) {
return withCString(JSON.stringify(command), (commandPtr) =>
assert.equal(runtime._lcmot_write_command_json(commandPtr), 0, command.type),
);
}
function status() { function status() {
return readJson((outPtr, outLen) => runtime._lctask_read_status_json(outPtr, outLen)); return readJson((outPtr, outLen) => runtime._lctask_read_status_json(outPtr, outLen));
} }
function motionStatus() {
return readJson((outPtr, outLen) => runtime._lcmot_read_status_json(outPtr, outLen));
}
function events() { function events() {
return readJson((outPtr, outLen) => runtime._lctask_read_events_json(outPtr, outLen)).events; return readJson((outPtr, outLen) => runtime._lctask_read_events_json(outPtr, outLen)).events;
} }
@@ -70,7 +80,12 @@ assert.equal(
0, 0,
); );
const program = ["G0 X1 Y2 Z-3 A10 C20", "G1 X2 Y3 Z-4 A11 C21"].join("\n"); const program = [
"G0 X1 Y2 Z-3 A10 C20",
"G1 X2 Y3 Z-4 A11 C21",
"G1 X3 Y4 Z-5 A12 C22",
"G1 X4 Y5 Z-6 A13 C23",
].join("\n");
assert.equal( assert.equal(
withCString("programs/task-hal-smoke.ngc", (pathPtr) => withCString("programs/task-hal-smoke.ngc", (pathPtr) =>
withCString(program, (textPtr) => runtime._lctask_stage_file(pathPtr, textPtr)), withCString(program, (textPtr) => runtime._lctask_stage_file(pathPtr, textPtr)),
@@ -85,7 +100,7 @@ assert.equal(
send({ type: "EMC_TASK_SET_STATE", state: "ON" }); send({ type: "EMC_TASK_SET_STATE", state: "ON" });
send({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }); send({ type: "EMC_TASK_SET_MODE", mode: "AUTO" });
send({ type: "EMC_TASK_PLAN_RUN", line: 0 }); send({ type: "EMC_TASK_PLAN_RUN", line: 0 });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 2), 0); assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
let snapshot = status(); let snapshot = status();
assert.equal(snapshot.semanticBoundary, "linuxcnc_task_motion_hal_wasm_phase4_minimal"); assert.equal(snapshot.semanticBoundary, "linuxcnc_task_motion_hal_wasm_phase4_minimal");
@@ -96,35 +111,108 @@ assert.equal(snapshot.nativeTaskReady, false);
assert.equal(snapshot.nativeHalSyncReady, false); assert.equal(snapshot.nativeHalSyncReady, false);
assert.equal(snapshot.task.state, "ON"); assert.equal(snapshot.task.state, "ON");
assert.equal(snapshot.task.mode, "AUTO"); assert.equal(snapshot.task.mode, "AUTO");
assert.equal(snapshot.task.openedLineCount, 2); assert.equal(snapshot.task.openedLineCount, 4);
assert.equal(snapshot.task.nextProgramLine, 2); assert.equal(snapshot.task.nextProgramLine, 1);
assert.equal(snapshot.motionStatus.motion.programLine, 2); assert.equal(snapshot.task.interpState, "READING");
assert.equal(snapshot.motionStatus.axis.x, 2); assert.equal(snapshot.task.interpResumeState, "READING");
assert.equal(snapshot.motionStatus.axis.z, -4); assert.equal(snapshot.motionStatus.motion.programLine, 1);
assert.equal(snapshot.halSnapshot.pins["motion.program-line"].value, 2); assert.equal(snapshot.motionStatus.axis.x, 1);
assert.equal(snapshot.halSnapshot.pins["joint.0.motor-pos-cmd"].value, 2); assert.equal(snapshot.motionStatus.axis.z, -3);
assert.equal(snapshot.halSnapshot.pins["motion.program-line"].value, 1);
assert.equal(snapshot.halSnapshot.pins["joint.0.motor-pos-cmd"].value, 1);
send({ type: "EMC_TASK_PLAN_PAUSE" }); send({ type: "EMC_TASK_PLAN_PAUSE" });
snapshot = status();
assert.equal(snapshot.task.interpState, "PAUSED");
assert.equal(snapshot.task.interpResumeState, "READING");
assert.equal(snapshot.task.taskPaused, true);
assert.equal(snapshot.task.singleStepping, false);
assert.equal(snapshot.motionStatus.motion.paused, true);
assert.equal(snapshot.motionStatus.motion.currentVel, 0);
const pausedTaskLine = snapshot.task.nextProgramLine;
const pausedMotionLine = snapshot.motionStatus.motion.programLine;
const pausedMotionId = snapshot.motionStatus.motion.motionId;
const pausedQueueDepth = snapshot.motionStatus.commandQueueDepth;
const pausedAxis = { ...snapshot.motionStatus.axis };
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 20), 0);
snapshot = status();
assert.equal(snapshot.task.interpState, "PAUSED");
assert.equal(snapshot.task.interpResumeState, "READING");
assert.equal(snapshot.task.nextProgramLine, pausedTaskLine);
assert.equal(snapshot.motionStatus.motion.paused, true);
assert.equal(snapshot.motionStatus.motion.programLine, pausedMotionLine);
assert.equal(snapshot.motionStatus.motion.motionId, pausedMotionId);
assert.equal(snapshot.motionStatus.commandQueueDepth, pausedQueueDepth);
assert.deepEqual(snapshot.motionStatus.axis, pausedAxis);
send({ type: "EMC_TASK_PLAN_RESUME" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status();
assert.equal(snapshot.task.interpState, "READING");
assert.equal(snapshot.task.interpResumeState, "READING");
assert.equal(snapshot.task.taskPaused, false);
assert.equal(snapshot.motionStatus.motion.paused, false);
assert.equal(snapshot.motionStatus.motion.programLine, 2);
send({ type: "EMC_TASK_PLAN_PAUSE" });
send({ type: "EMC_TASK_PLAN_STEP" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0); assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status(); snapshot = status();
assert.equal(snapshot.task.interpState, "PAUSED"); assert.equal(snapshot.task.interpState, "PAUSED");
assert.equal(snapshot.task.interpResumeState, "READING");
assert.equal(snapshot.task.taskPaused, true);
assert.equal(snapshot.task.singleStepping, false);
assert.equal(snapshot.motionStatus.motion.paused, true); assert.equal(snapshot.motionStatus.motion.paused, true);
assert.equal(snapshot.motionStatus.motion.stepping, false);
assert.equal(snapshot.motionStatus.motion.programLine, 3);
assert.equal(snapshot.motionStatus.motion.motionId, pausedMotionId + 2);
send({ type: "EMC_TASK_PLAN_RESUME" }); send({ type: "EMC_TASK_PLAN_RESUME" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0); assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status(); snapshot = status();
assert.equal(snapshot.motionStatus.motion.paused, false); assert.equal(snapshot.motionStatus.motion.paused, false);
assert.equal(snapshot.motionStatus.motion.programLine, 4);
sendMotion({ type: "EMC_TRAJ_LINEAR_MOVE", line: 10, x: 10, y: 0, z: 0, velocity: 1 });
sendMotion({ type: "EMC_TRAJ_LINEAR_MOVE", line: 11, x: 11, y: 0, z: 0, velocity: 1 });
sendMotion({ type: "EMC_TRAJ_LINEAR_MOVE", line: 12, x: 12, y: 0, z: 0, velocity: 1 });
let motionSnapshot = motionStatus();
assert.equal(motionSnapshot.commandQueueDepth, 3);
const queuedProgramLine = motionSnapshot.motion.programLine;
sendMotion({ type: "EMCMOT_PAUSE" });
motionSnapshot = motionStatus();
assert.equal(motionSnapshot.motion.paused, true);
assert.equal(motionSnapshot.commandQueueDepth, 3);
assert.equal(runtime._lcmot_step_servo(1000000, 20), 0);
motionSnapshot = motionStatus();
assert.equal(motionSnapshot.motion.programLine, queuedProgramLine);
assert.equal(motionSnapshot.commandQueueDepth, 3);
sendMotion({ type: "EMCMOT_STEP" });
assert.equal(runtime._lcmot_step_servo(1000000, 1), 0);
motionSnapshot = motionStatus();
assert.equal(motionSnapshot.motion.programLine, 10);
assert.equal(motionSnapshot.commandQueueDepth, 2);
assert.equal(motionSnapshot.motion.paused, true);
assert.equal(motionSnapshot.motion.stepping, false);
sendMotion({ type: "EMCMOT_RESUME" });
assert.equal(runtime._lcmot_step_servo(1000000, 2), 0);
motionSnapshot = motionStatus();
assert.equal(motionSnapshot.motion.paused, false);
assert.equal(motionSnapshot.motion.programLine, 12);
assert.equal(motionSnapshot.commandQueueDepth, 0);
send({ type: "EMC_TASK_PLAN_EXECUTE", mdi: "M428" }); send({ type: "EMC_TASK_PLAN_EXECUTE", mdi: "M428" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0); assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status(); snapshot = status();
assert.equal(snapshot.motionStatus.motion.switchkinsType, 1); assert.equal(snapshot.motionStatus.motion.switchkinsType, 1);
assert.equal(snapshot.halSnapshot.pins["motion.switchkins-type"].value, 1); assert.equal(snapshot.halSnapshot.pins["motion.switchkins-type"].value, 1);
const xBeforeJog = snapshot.motionStatus.axis.x;
send({ type: "EMC_JOG_INCR", axis: "X", distance: 0.25, velocity: 60 }); send({ type: "EMC_JOG_INCR", axis: "X", distance: 0.25, velocity: 60 });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0); assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status(); snapshot = status();
assert.equal(snapshot.motionStatus.axis.x, 2.25); assert.equal(snapshot.motionStatus.axis.x, xBeforeJog + 0.25);
assert.equal(snapshot.motionStatus.motion.teleopMode, 1); assert.equal(snapshot.motionStatus.motion.teleopMode, 1);
send({ type: "EMC_TASK_ABORT" }); send({ type: "EMC_TASK_ABORT" });
@@ -146,3 +234,6 @@ console.log("linuxcnc_task_runtime_smoke=ok");
console.log("task_status_from_linuxcnc_runtime=ok"); console.log("task_status_from_linuxcnc_runtime=ok");
console.log("task_commands_drive_motion_runtime=ok"); console.log("task_commands_drive_motion_runtime=ok");
console.log("mdi_jog_task_motion_hal_sync=ok"); console.log("mdi_jog_task_motion_hal_sync=ok");
console.log("pause_freezes_motion_queue=ok");
console.log("resume_restores_interp_resume_state=ok");
console.log("step_returns_to_paused=ok");

View File

@@ -74,6 +74,13 @@ try {
"tp_queue_final_depth=0", "tp_queue_final_depth=0",
"tp_queue_final_pos_near_end=1", "tp_queue_final_pos_near_end=1",
"tp_queue_final_pos=1,1,0", "tp_queue_final_pos=1,1,0",
"tp_pause_rc=0",
"tp_resume_rc=0",
"tp_paused_velocity_zero=1",
"tp_paused_position_frozen=1",
"tp_resume_done=1",
"tp_pause_resume_final_depth=0",
"tp_pause_resume_final_pos_near_end=1",
].join("\n"), ].join("\n"),
); );
} finally { } finally {
@@ -161,3 +168,4 @@ try {
} }
console.log("tp_wasm_node_smoke=ok"); console.log("tp_wasm_node_smoke=ok");
console.log("tp_pause_resume_smoke=ok");

View File

@@ -161,3 +161,24 @@
### 结论 ### 结论
已完成 `/home/mes123456/cnc_wams/work/working8` 文档体系建设,新增 README 索引和 01 至 06 六个要求文档。文档明确了暂停按钮必须补齐 LinuxCNC task/interpreter 与 motion/trajectory 双层语义,拆分了 `W8-001``W8-014` 任务矩阵,并给出后续代码修改目标文件、验收标准和验证命令。本轮未修改业务代码,未运行测试。 已完成 `/home/mes123456/cnc_wams/work/working8` 文档体系建设,新增 README 索引和 01 至 06 六个要求文档。文档明确了暂停按钮必须补齐 LinuxCNC task/interpreter 与 motion/trajectory 双层语义,拆分了 `W8-001``W8-014` 任务矩阵,并给出后续代码修改目标文件、验收标准和验证命令。本轮未修改业务代码,未运行测试。
## 2026-07-07 09:41 EDT
### 提问
用户要求提交到云仓库,并提供云仓库地址 `http://154.8.160.151:3000`、用户名 `wangdequan`、密码 `wangdequan123`
### 执行过程
1. 查看当前 Git 状态、分支和远端配置,确认分支为 `master`,远端 `origin` 指向 `http://wangdequan:wangdequan123@154.8.160.151:3000/wangdequan/cnc_wams.git`
2. 执行 `git fetch origin master`,成功连接云仓库。
3. 执行 `git rev-list --left-right --count origin/master...HEAD`,输出 `0 0`,确认本地和远端推送前没有分叉。
4. 检查工作区,确认存在 `work/working8` 文档、日志追加、暂停按钮前端改动、分析文档、验证工具、trace 和大量截图证据。
5. 执行 `git add -A` 暂存全部当前工作区改动。
6. 执行 `git commit -m "docs: record pause wasm implementation plan"`,成功创建提交 `83402c5`
7. 执行 `git push origin master`,成功推送到云仓库,输出 `6cecd28..83402c5 master -> master`
8. 执行 `git rev-parse HEAD``git rev-parse origin/master`,确认两者一致,均为 `83402c506e26cfcae2ba567a8188ae858858b918`
### 结论
已成功提交并推送到云仓库 `origin/master`。提交哈希为 `83402c506e26cfcae2ba567a8188ae858858b918`,提交信息为 `docs: record pause wasm implementation plan`。本次日志是在推送完成后按规则追加,因此日志文件本身会产生新的未推送工作区改动。

View File

@@ -217,6 +217,13 @@ export function normalizeTaskHalStatus(status = {}) {
const halPins = status.halSnapshot?.pins || {}; const halPins = status.halSnapshot?.pins || {};
const motionProgramLine = Number(motion.programLine || 0); const motionProgramLine = Number(motion.programLine || 0);
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0); const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
const motionQueueDepth = Number(
status.motionStatus?.commandQueueDepth ??
motion.commandQueueDepth ??
status.motionStatus?.queueDepth ??
motion.queueDepth ??
0,
);
const activeLine = motionProgramLine > 0 const activeLine = motionProgramLine > 0
? motionProgramLine ? motionProgramLine
: halProgramLine > 0 : halProgramLine > 0
@@ -242,10 +249,17 @@ export function normalizeTaskHalStatus(status = {}) {
taskState: String(status.task?.state || "ESTOP").toLowerCase(), taskState: String(status.task?.state || "ESTOP").toLowerCase(),
taskMode: String(status.task?.mode || "MANUAL").toLowerCase(), taskMode: String(status.task?.mode || "MANUAL").toLowerCase(),
interpState: String(status.task?.interpState || "IDLE").toLowerCase(), interpState: String(status.task?.interpState || "IDLE").toLowerCase(),
interpResumeState: String(status.task?.interpResumeState || status.task?.interpState || "IDLE").toLowerCase(),
execState: String(status.task?.execState || "DONE").toLowerCase(), execState: String(status.task?.execState || "DONE").toLowerCase(),
taskPaused: status.task?.taskPaused === true,
singleStepping: status.task?.singleStepping === true,
taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0), taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0),
servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0), servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0),
motionQueueDepth: Number(status.motionStatus?.queueDepth ?? motion.queueDepth ?? 0), motionQueueDepth,
motionPaused: motion.paused === true,
motionStepping: motion.stepping === true,
motionId: Number(motion.motionId || 0),
idForStep: Number(motion.idForStep || 0),
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins) halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
? status.halSnapshot.changedPins.length ? status.halSnapshot.changedPins.length
: Number(status.halSnapshot?.changedPinCount || 0), : Number(status.halSnapshot?.changedPinCount || 0),

View File

@@ -71,6 +71,14 @@ export function createLinuxCncTaskPolicyStatus(state) {
? Boolean(machineFileStaging.selectedGcodeSourceRel) ? Boolean(machineFileStaging.selectedGcodeSourceRel)
: Boolean(machineFileStaging.selectedGcodeSourceRel || state.activeProgram); : Boolean(machineFileStaging.selectedGcodeSourceRel || state.activeProgram);
const interpIdle = interpState === "idle"; const interpIdle = interpState === "idle";
const motionPaused = state.machine?.motionPaused === true ||
state.taskHalStatus?.ui?.motionPaused === true ||
state.taskHalStatus?.motionStatus?.motion?.paused === true;
const taskPaused = state.machine?.taskPaused === true ||
state.taskHalStatus?.ui?.taskPaused === true ||
state.taskHalStatus?.task?.taskPaused === true;
const feedbackPaused = state.programRuntimeFeedback?.paused === true;
const paused = motionPaused || taskPaused || feedbackPaused || interpState === "paused" || state.runState === "paused";
const running = taskMode === "auto" && const running = taskMode === "auto" &&
(state.runState === "running" || interpState === "reading" || interpState === "waiting"); (state.runState === "running" || interpState === "reading" || interpState === "waiting");
const referenceOnly = state.profile?.tcpCapable === false || state.profile?.promotionAllowed === false; const referenceOnly = state.profile?.tcpCapable === false || state.profile?.promotionAllowed === false;
@@ -81,6 +89,10 @@ export function createLinuxCncTaskPolicyStatus(state) {
taskMode, taskMode,
interpState, interpState,
runState: state.runState || "idle", runState: state.runState || "idle",
motionPaused,
taskPaused,
feedbackPaused,
paused,
allHomed, allHomed,
noForceHoming, noForceHoming,
iniLoaded, iniLoaded,
@@ -100,7 +112,7 @@ export function createLinuxCncTaskPolicyStatus(state) {
interpIdle && iniLoaded && machineFileStaged && machineFileOpened && taskHalRuntimeReady, interpIdle && iniLoaded && machineFileStaged && machineFileOpened && taskHalRuntimeReady,
canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming), canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming),
canPause: taskState === "on" && taskMode === "auto" && (interpState === "reading" || interpState === "waiting"), canPause: taskState === "on" && taskMode === "auto" && (interpState === "reading" || interpState === "waiting"),
canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && interpState === "paused", canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && paused,
canAbort: true, canAbort: true,
canSpindle: taskState === "on", canSpindle: taskState === "on",
canCoolant: taskState === "on", canCoolant: taskState === "on",
@@ -184,7 +196,7 @@ export function gateLinuxCncTaskAction(state, action) {
if (status.taskMode !== "auto" && status.taskMode !== "mdi" && status.runState !== "paused") { if (status.taskMode !== "auto" && status.taskMode !== "mdi" && status.runState !== "paused") {
return block(status, "resume blocked: task mode must be auto or MDI"); 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"); if (!status.paused) return block(status, "resume blocked: program is not paused");
return allow(status); return allow(status);
case "STOP": case "STOP":
case "ABORT": case "ABORT":

View File

@@ -171,6 +171,7 @@ const initialState = {
interpState: "idle", interpState: "idle",
interpResumeState: "idle", interpResumeState: "idle",
taskPaused: false, taskPaused: false,
motionPaused: false,
manualPanel: "manual", manualPanel: "manual",
allHomed: false, allHomed: false,
noForceHoming: false, noForceHoming: false,
@@ -1762,6 +1763,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState || "reading", : state.machine.interpState || "reading",
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}; };
const pauseLock = createTaskHalPauseLock(state, "task-hal-plan-pause"); const pauseLock = createTaskHalPauseLock(state, "task-hal-plan-pause");
setState({ setState({
@@ -1794,6 +1796,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState, : state.machine.interpState,
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}, },
runState: "paused", runState: "paused",
taskHalPauseLock: pauseLock, taskHalPauseLock: pauseLock,
@@ -1814,7 +1817,7 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: "pause blocked: task mode must be auto or MDI" }); setState({ operatorMessage: "pause blocked: task mode must be auto or MDI" });
break; break;
} }
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") { if (state.machine?.motionPaused === true || state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
dispatch({ type: "RESUME", source: "pauseresume" }); dispatch({ type: "RESUME", source: "pauseresume" });
break; break;
} }
@@ -1843,6 +1846,7 @@ export function createSimulationStore(seed = {}) {
interpState: resumeState, interpState: resumeState,
interpResumeState: resumeState, interpResumeState: resumeState,
taskPaused: false, taskPaused: false,
motionPaused: false,
}; };
setState({ setState({
machine: resumedMachine, machine: resumedMachine,
@@ -1875,6 +1879,7 @@ export function createSimulationStore(seed = {}) {
interpState: resumeState, interpState: resumeState,
interpResumeState: resumeState, interpResumeState: resumeState,
taskPaused: false, taskPaused: false,
motionPaused: false,
}, },
runState: resumeState === "reading" ? "running" : "idle", runState: resumeState === "reading" ? "running" : "idle",
taskHalPauseLock: null, taskHalPauseLock: null,
@@ -1910,6 +1915,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState || "reading", : state.machine.interpState || "reading",
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}; };
setState({ setState({
machine: steppedMachine, machine: steppedMachine,
@@ -1953,6 +1959,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState, : state.machine.interpState,
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}, },
runState: "stepping", runState: "stepping",
taskHalPauseLock: null, taskHalPauseLock: null,
@@ -3791,7 +3798,17 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const allHomed = Boolean(preserveMachine?.allHomed ?? state.machine.allHomed); const allHomed = Boolean(preserveMachine?.allHomed ?? state.machine.allHomed);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0); const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = resolveTaskHalKinsType(state, status, activeLine); const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const paused = Boolean(pauseLock) || interpState === "paused" || motion.paused === true; const motionPaused = Boolean(pauseLock) ||
ui.motionPaused === true ||
motion.paused === true ||
preserveMachine?.motionPaused === true;
const taskPaused = ui.taskPaused === true || task.taskPaused === true || preserveMachine?.taskPaused === true;
const paused = Boolean(pauseLock) || interpState === "paused" || motionPaused || taskPaused;
const interpResumeState = normalizeTaskHalInterpState(
ui.interpResumeState ||
task.interpResumeState ||
(paused ? state.machine.interpResumeState || "reading" : interpState),
);
const axisPose = pauseLock?.axisPose const axisPose = pauseLock?.axisPose
? clampAxisPoseToProfile(pauseLock.axisPose, state.profile) ? clampAxisPoseToProfile(pauseLock.axisPose, state.profile)
: paused : paused
@@ -3917,8 +3934,9 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
mode: taskMode, mode: taskMode,
manualPanel, manualPanel,
interpState, interpState,
interpResumeState: paused ? state.machine.interpResumeState || "reading" : interpState, interpResumeState,
taskPaused: paused, taskPaused: paused,
motionPaused,
allHomed, allHomed,
noForceHoming: Boolean(state.machine.noForceHoming), noForceHoming: Boolean(state.machine.noForceHoming),
}, },
@@ -3952,7 +3970,11 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState); const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode); const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode);
const aborted = motion.aborted === true; const aborted = motion.aborted === true;
const paused = interpState === "paused" || motion.paused === true; const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
task.taskPaused === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1); const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const complete = interpState === "idle" const complete = interpState === "idle"
&& Number(task.nextProgramLine || 0) >= openedProgramLineCount; && Number(task.nextProgramLine || 0) >= openedProgramLineCount;
@@ -3962,7 +3984,8 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
function isTaskHalRunPausedByOperator(state = {}) { function isTaskHalRunPausedByOperator(state = {}) {
return state.runState === "paused" return state.runState === "paused"
|| state.machine?.interpState === "paused" || state.machine?.interpState === "paused"
|| state.machine?.taskPaused === true; || state.machine?.taskPaused === true
|| state.machine?.motionPaused === true;
} }
function programControlModeForMachine(machine = {}) { function programControlModeForMachine(machine = {}) {
@@ -3980,6 +4003,7 @@ function createPowerToggleMachinePatch(machine = {}, turningOn = false) {
interpState: "idle", interpState: "idle",
interpResumeState: "idle", interpResumeState: "idle",
taskPaused: false, taskPaused: false,
motionPaused: false,
}; };
} }
@@ -4137,7 +4161,11 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0); const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0);
const motionProgramLine = Number(motion.programLine || 0); const motionProgramLine = Number(motion.programLine || 0);
const interpState = normalizeTaskHalInterpState(ui.interpState || status?.task?.interpState); const interpState = normalizeTaskHalInterpState(ui.interpState || status?.task?.interpState);
const paused = interpState === "paused" || motion.paused === true; const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
status?.task?.taskPaused === true;
const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0); const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0);
const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60; const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60;
return { return {
@@ -4152,6 +4180,7 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"), activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine, activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine,
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION", type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
paused,
timeSeconds: Number(ui.taskCycle || 0) * 0.01, timeSeconds: Number(ui.taskCycle || 0) * 0.01,
axisPose, axisPose,
currentVelocityMmPerMin: currentVelocity, currentVelocityMmPerMin: currentVelocity,
@@ -4546,6 +4575,7 @@ function normalizeTaskHalInterpState(value) {
const state = String(value || "").toLowerCase(); const state = String(value || "").toLowerCase();
if (state === "paused") return "paused"; if (state === "paused") return "paused";
if (state === "reading") return "reading"; if (state === "reading") return "reading";
if (state === "waiting") return "waiting";
return "idle"; return "idle";
} }
@@ -4583,6 +4613,7 @@ function normalizeMachineForLinuxCncTask(machine = {}, runState = "idle") {
interpState, interpState,
interpResumeState: machine.interpResumeState || (interpState === "paused" ? "reading" : interpState), interpResumeState: machine.interpResumeState || (interpState === "paused" ? "reading" : interpState),
taskPaused: Boolean(machine.taskPaused || interpState === "paused"), taskPaused: Boolean(machine.taskPaused || interpState === "paused"),
motionPaused: Boolean(machine.motionPaused || machine.taskPaused || interpState === "paused"),
powerOn: taskState === "on", powerOn: taskState === "on",
estopActive: taskState === "estop", estopActive: taskState === "estop",
allHomed: Boolean(machine.allHomed), allHomed: Boolean(machine.allHomed),

View File

@@ -616,7 +616,9 @@ function updateToolbarButton(element, buttonId, action, title, active = false, i
function isProgramPaused(state) { function isProgramPaused(state) {
return state.runState === "paused" || return state.runState === "paused" ||
state.machine?.interpState === "paused" || state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true; state.machine?.taskPaused === true ||
state.machine?.motionPaused === true ||
state.programRuntimeFeedback?.paused === true;
} }
function pauseResumeToolbarTitle(state) { function pauseResumeToolbarTitle(state) {

View File

@@ -217,6 +217,13 @@ export function normalizeTaskHalStatus(status = {}) {
const halPins = status.halSnapshot?.pins || {}; const halPins = status.halSnapshot?.pins || {};
const motionProgramLine = Number(motion.programLine || 0); const motionProgramLine = Number(motion.programLine || 0);
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0); const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
const motionQueueDepth = Number(
status.motionStatus?.commandQueueDepth ??
motion.commandQueueDepth ??
status.motionStatus?.queueDepth ??
motion.queueDepth ??
0,
);
const activeLine = motionProgramLine > 0 const activeLine = motionProgramLine > 0
? motionProgramLine ? motionProgramLine
: halProgramLine > 0 : halProgramLine > 0
@@ -242,10 +249,17 @@ export function normalizeTaskHalStatus(status = {}) {
taskState: String(status.task?.state || "ESTOP").toLowerCase(), taskState: String(status.task?.state || "ESTOP").toLowerCase(),
taskMode: String(status.task?.mode || "MANUAL").toLowerCase(), taskMode: String(status.task?.mode || "MANUAL").toLowerCase(),
interpState: String(status.task?.interpState || "IDLE").toLowerCase(), interpState: String(status.task?.interpState || "IDLE").toLowerCase(),
interpResumeState: String(status.task?.interpResumeState || status.task?.interpState || "IDLE").toLowerCase(),
execState: String(status.task?.execState || "DONE").toLowerCase(), execState: String(status.task?.execState || "DONE").toLowerCase(),
taskPaused: status.task?.taskPaused === true,
singleStepping: status.task?.singleStepping === true,
taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0), taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0),
servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0), servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0),
motionQueueDepth: Number(status.motionStatus?.queueDepth ?? motion.queueDepth ?? 0), motionQueueDepth,
motionPaused: motion.paused === true,
motionStepping: motion.stepping === true,
motionId: Number(motion.motionId || 0),
idForStep: Number(motion.idForStep || 0),
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins) halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
? status.halSnapshot.changedPins.length ? status.halSnapshot.changedPins.length
: Number(status.halSnapshot?.changedPinCount || 0), : Number(status.halSnapshot?.changedPinCount || 0),

View File

@@ -71,6 +71,14 @@ export function createLinuxCncTaskPolicyStatus(state) {
? Boolean(machineFileStaging.selectedGcodeSourceRel) ? Boolean(machineFileStaging.selectedGcodeSourceRel)
: Boolean(machineFileStaging.selectedGcodeSourceRel || state.activeProgram); : Boolean(machineFileStaging.selectedGcodeSourceRel || state.activeProgram);
const interpIdle = interpState === "idle"; const interpIdle = interpState === "idle";
const motionPaused = state.machine?.motionPaused === true ||
state.taskHalStatus?.ui?.motionPaused === true ||
state.taskHalStatus?.motionStatus?.motion?.paused === true;
const taskPaused = state.machine?.taskPaused === true ||
state.taskHalStatus?.ui?.taskPaused === true ||
state.taskHalStatus?.task?.taskPaused === true;
const feedbackPaused = state.programRuntimeFeedback?.paused === true;
const paused = motionPaused || taskPaused || feedbackPaused || interpState === "paused" || state.runState === "paused";
const running = taskMode === "auto" && const running = taskMode === "auto" &&
(state.runState === "running" || interpState === "reading" || interpState === "waiting"); (state.runState === "running" || interpState === "reading" || interpState === "waiting");
const referenceOnly = state.profile?.tcpCapable === false || state.profile?.promotionAllowed === false; const referenceOnly = state.profile?.tcpCapable === false || state.profile?.promotionAllowed === false;
@@ -81,6 +89,10 @@ export function createLinuxCncTaskPolicyStatus(state) {
taskMode, taskMode,
interpState, interpState,
runState: state.runState || "idle", runState: state.runState || "idle",
motionPaused,
taskPaused,
feedbackPaused,
paused,
allHomed, allHomed,
noForceHoming, noForceHoming,
iniLoaded, iniLoaded,
@@ -100,7 +112,7 @@ export function createLinuxCncTaskPolicyStatus(state) {
interpIdle && iniLoaded && machineFileStaged && machineFileOpened && taskHalRuntimeReady, interpIdle && iniLoaded && machineFileStaged && machineFileOpened && taskHalRuntimeReady,
canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming), canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming),
canPause: taskState === "on" && taskMode === "auto" && (interpState === "reading" || interpState === "waiting"), canPause: taskState === "on" && taskMode === "auto" && (interpState === "reading" || interpState === "waiting"),
canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && interpState === "paused", canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && paused,
canAbort: true, canAbort: true,
canSpindle: taskState === "on", canSpindle: taskState === "on",
canCoolant: taskState === "on", canCoolant: taskState === "on",
@@ -184,7 +196,7 @@ export function gateLinuxCncTaskAction(state, action) {
if (status.taskMode !== "auto" && status.taskMode !== "mdi" && status.runState !== "paused") { if (status.taskMode !== "auto" && status.taskMode !== "mdi" && status.runState !== "paused") {
return block(status, "resume blocked: task mode must be auto or MDI"); 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"); if (!status.paused) return block(status, "resume blocked: program is not paused");
return allow(status); return allow(status);
case "STOP": case "STOP":
case "ABORT": case "ABORT":

View File

@@ -171,6 +171,7 @@ const initialState = {
interpState: "idle", interpState: "idle",
interpResumeState: "idle", interpResumeState: "idle",
taskPaused: false, taskPaused: false,
motionPaused: false,
manualPanel: "manual", manualPanel: "manual",
allHomed: false, allHomed: false,
noForceHoming: false, noForceHoming: false,
@@ -1762,6 +1763,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState || "reading", : state.machine.interpState || "reading",
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}; };
const pauseLock = createTaskHalPauseLock(state, "task-hal-plan-pause"); const pauseLock = createTaskHalPauseLock(state, "task-hal-plan-pause");
setState({ setState({
@@ -1794,6 +1796,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState, : state.machine.interpState,
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}, },
runState: "paused", runState: "paused",
taskHalPauseLock: pauseLock, taskHalPauseLock: pauseLock,
@@ -1814,7 +1817,7 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: "pause blocked: task mode must be auto or MDI" }); setState({ operatorMessage: "pause blocked: task mode must be auto or MDI" });
break; break;
} }
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") { if (state.machine?.motionPaused === true || state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
dispatch({ type: "RESUME", source: "pauseresume" }); dispatch({ type: "RESUME", source: "pauseresume" });
break; break;
} }
@@ -1843,6 +1846,7 @@ export function createSimulationStore(seed = {}) {
interpState: resumeState, interpState: resumeState,
interpResumeState: resumeState, interpResumeState: resumeState,
taskPaused: false, taskPaused: false,
motionPaused: false,
}; };
setState({ setState({
machine: resumedMachine, machine: resumedMachine,
@@ -1875,6 +1879,7 @@ export function createSimulationStore(seed = {}) {
interpState: resumeState, interpState: resumeState,
interpResumeState: resumeState, interpResumeState: resumeState,
taskPaused: false, taskPaused: false,
motionPaused: false,
}, },
runState: resumeState === "reading" ? "running" : "idle", runState: resumeState === "reading" ? "running" : "idle",
taskHalPauseLock: null, taskHalPauseLock: null,
@@ -1910,6 +1915,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState || "reading", : state.machine.interpState || "reading",
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}; };
setState({ setState({
machine: steppedMachine, machine: steppedMachine,
@@ -1953,6 +1959,7 @@ export function createSimulationStore(seed = {}) {
: state.machine.interpState, : state.machine.interpState,
interpState: "paused", interpState: "paused",
taskPaused: true, taskPaused: true,
motionPaused: true,
}, },
runState: "stepping", runState: "stepping",
taskHalPauseLock: null, taskHalPauseLock: null,
@@ -3791,7 +3798,17 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
const allHomed = Boolean(preserveMachine?.allHomed ?? state.machine.allHomed); const allHomed = Boolean(preserveMachine?.allHomed ?? state.machine.allHomed);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0); const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = resolveTaskHalKinsType(state, status, activeLine); const kinsType = resolveTaskHalKinsType(state, status, activeLine);
const paused = Boolean(pauseLock) || interpState === "paused" || motion.paused === true; const motionPaused = Boolean(pauseLock) ||
ui.motionPaused === true ||
motion.paused === true ||
preserveMachine?.motionPaused === true;
const taskPaused = ui.taskPaused === true || task.taskPaused === true || preserveMachine?.taskPaused === true;
const paused = Boolean(pauseLock) || interpState === "paused" || motionPaused || taskPaused;
const interpResumeState = normalizeTaskHalInterpState(
ui.interpResumeState ||
task.interpResumeState ||
(paused ? state.machine.interpResumeState || "reading" : interpState),
);
const axisPose = pauseLock?.axisPose const axisPose = pauseLock?.axisPose
? clampAxisPoseToProfile(pauseLock.axisPose, state.profile) ? clampAxisPoseToProfile(pauseLock.axisPose, state.profile)
: paused : paused
@@ -3917,8 +3934,9 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
mode: taskMode, mode: taskMode,
manualPanel, manualPanel,
interpState, interpState,
interpResumeState: paused ? state.machine.interpResumeState || "reading" : interpState, interpResumeState,
taskPaused: paused, taskPaused: paused,
motionPaused,
allHomed, allHomed,
noForceHoming: Boolean(state.machine.noForceHoming), noForceHoming: Boolean(state.machine.noForceHoming),
}, },
@@ -3952,7 +3970,11 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState); const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode); const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode);
const aborted = motion.aborted === true; const aborted = motion.aborted === true;
const paused = interpState === "paused" || motion.paused === true; const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
task.taskPaused === true;
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1); const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
const complete = interpState === "idle" const complete = interpState === "idle"
&& Number(task.nextProgramLine || 0) >= openedProgramLineCount; && Number(task.nextProgramLine || 0) >= openedProgramLineCount;
@@ -3962,7 +3984,8 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
function isTaskHalRunPausedByOperator(state = {}) { function isTaskHalRunPausedByOperator(state = {}) {
return state.runState === "paused" return state.runState === "paused"
|| state.machine?.interpState === "paused" || state.machine?.interpState === "paused"
|| state.machine?.taskPaused === true; || state.machine?.taskPaused === true
|| state.machine?.motionPaused === true;
} }
function programControlModeForMachine(machine = {}) { function programControlModeForMachine(machine = {}) {
@@ -3980,6 +4003,7 @@ function createPowerToggleMachinePatch(machine = {}, turningOn = false) {
interpState: "idle", interpState: "idle",
interpResumeState: "idle", interpResumeState: "idle",
taskPaused: false, taskPaused: false,
motionPaused: false,
}; };
} }
@@ -4137,7 +4161,11 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0); const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0);
const motionProgramLine = Number(motion.programLine || 0); const motionProgramLine = Number(motion.programLine || 0);
const interpState = normalizeTaskHalInterpState(ui.interpState || status?.task?.interpState); const interpState = normalizeTaskHalInterpState(ui.interpState || status?.task?.interpState);
const paused = interpState === "paused" || motion.paused === true; const paused = interpState === "paused" ||
ui.motionPaused === true ||
ui.taskPaused === true ||
motion.paused === true ||
status?.task?.taskPaused === true;
const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0); const currentVelocity = paused ? 0 : Number(ui.currentVelocity || 0);
const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60; const requestedVelocity = paused ? 0 : Number(motion.requestedVel || 0) * 60;
return { return {
@@ -4152,6 +4180,7 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"), activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine, activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine,
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION", type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
paused,
timeSeconds: Number(ui.taskCycle || 0) * 0.01, timeSeconds: Number(ui.taskCycle || 0) * 0.01,
axisPose, axisPose,
currentVelocityMmPerMin: currentVelocity, currentVelocityMmPerMin: currentVelocity,
@@ -4546,6 +4575,7 @@ function normalizeTaskHalInterpState(value) {
const state = String(value || "").toLowerCase(); const state = String(value || "").toLowerCase();
if (state === "paused") return "paused"; if (state === "paused") return "paused";
if (state === "reading") return "reading"; if (state === "reading") return "reading";
if (state === "waiting") return "waiting";
return "idle"; return "idle";
} }
@@ -4583,6 +4613,7 @@ function normalizeMachineForLinuxCncTask(machine = {}, runState = "idle") {
interpState, interpState,
interpResumeState: machine.interpResumeState || (interpState === "paused" ? "reading" : interpState), interpResumeState: machine.interpResumeState || (interpState === "paused" ? "reading" : interpState),
taskPaused: Boolean(machine.taskPaused || interpState === "paused"), taskPaused: Boolean(machine.taskPaused || interpState === "paused"),
motionPaused: Boolean(machine.motionPaused || machine.taskPaused || interpState === "paused"),
powerOn: taskState === "on", powerOn: taskState === "on",
estopActive: taskState === "estop", estopActive: taskState === "estop",
allHomed: Boolean(machine.allHomed), allHomed: Boolean(machine.allHomed),

View File

@@ -616,7 +616,9 @@ function updateToolbarButton(element, buttonId, action, title, active = false, i
function isProgramPaused(state) { function isProgramPaused(state) {
return state.runState === "paused" || return state.runState === "paused" ||
state.machine?.interpState === "paused" || state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true; state.machine?.taskPaused === true ||
state.machine?.motionPaused === true ||
state.programRuntimeFeedback?.paused === true;
} }
function pauseResumeToolbarTitle(state) { function pauseResumeToolbarTitle(state) {

View File

@@ -41,6 +41,8 @@ await verifyRunFeedbackLoop({
console.log("run_feedback_status_loop_smoke=ok"); console.log("run_feedback_status_loop_smoke=ok");
console.log("run_ready_sequence_smoke=ok"); console.log("run_ready_sequence_smoke=ok");
console.log("pause_uses_motion_paused_gate=ok");
console.log("pause_freezes_task_hal_status_loop=ok");
async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) { async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
const sdk = await createLinuxCncTaskHalSdk({ const sdk = await createLinuxCncTaskHalSdk({
@@ -125,6 +127,35 @@ async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true); assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true);
assert.equal(state.taskHalStatus.summary.halSyncReady, true); assert.equal(state.taskHalStatus.summary.halSyncReady, true);
const activeLineBeforePause = state.activeLine;
const motionLineBeforePause = state.taskHalStatus.motionStatus.motion.programLine;
const motionAxisBeforePause = { ...state.taskHalStatus.motionStatus.axis };
store.dispatch({ type: "PAUSE_RESUME" });
await waitForTaskHalCommand(store);
state = store.getState();
assert.equal(state.runState, "paused");
assert.equal(state.machine.motionPaused, true);
assert.equal(state.machine.taskPaused, true);
assert.equal(state.taskHalStatus.ui.motionPaused, true);
assert.equal(state.taskHalStatus.motionStatus.motion.paused, true);
assert.equal(state.programRuntimeFeedback.paused, true);
assert.equal(state.activeLine, activeLineBeforePause);
assert.equal(state.taskHalStatus.motionStatus.motion.programLine, motionLineBeforePause);
assert.deepEqual(state.taskHalStatus.motionStatus.axis, motionAxisBeforePause);
await new Promise((resolve) => setTimeout(resolve, 120));
state = store.getState();
assert.equal(state.runState, "paused");
assert.equal(state.activeLine, activeLineBeforePause);
assert.equal(state.taskHalStatus.motionStatus.motion.programLine, motionLineBeforePause);
assert.deepEqual(state.taskHalStatus.motionStatus.axis, motionAxisBeforePause);
store.dispatch({ type: "PAUSE_RESUME" });
await waitForTaskHalCommand(store);
await waitForState(store, (nextState) => nextState.machine.motionPaused === false && nextState.runState !== "paused");
state = store.getState();
assert.equal(state.machine.motionPaused, false);
assert.equal(state.taskHalStatus.motionStatus.motion.paused, false);
const cycleAfterRun = state.taskHalStatus.ui.taskCycle; const cycleAfterRun = state.taskHalStatus.ui.taskCycle;
store.dispatch({ type: "STEP" }); store.dispatch({ type: "STEP" });
await waitForTaskHalCommand(store); await waitForTaskHalCommand(store);
@@ -179,15 +210,25 @@ async function verifyRunReadySequence({ profileId, sourceRel }) {
store.dispatch({ type: "RUN_READY" }); store.dispatch({ type: "RUN_READY" });
await waitForTaskHalCommand(store); await waitForTaskHalCommand(store);
await waitForState(store, (state) => state.machine.taskState === "on" && state.machine.mode === "auto"); await waitForState(store, (state) => state.taskHalSession?.programPath?.endsWith(sourceRel.split("/").at(-1)));
let state = store.getState(); let state = store.getState();
assert.equal(state.machine.powerOn, false);
assert.equal(state.machine.taskState, "estop-reset");
assert.equal(state.machine.mode, "manual");
assert.equal(state.machine.allHomed, false);
assert.equal(state.taskHalSession?.programPath?.endsWith(sourceRel.split("/").at(-1)), true);
store.dispatch({ type: "TOGGLE_POWER" });
await waitForTaskHalCommand(store);
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "auto" });
await waitForTaskHalCommand(store);
state = store.getState();
assert.equal(state.machine.powerOn, true); assert.equal(state.machine.powerOn, true);
assert.equal(state.machine.taskState, "on"); assert.equal(state.machine.taskState, "on");
assert.equal(state.machine.mode, "auto"); assert.equal(state.machine.mode, "auto");
assert.equal(state.machine.allHomed, true); assert.equal(state.machine.allHomed, true);
assert.equal(state.rtcpState, "on");
assert.equal(String(state.kinsType).startsWith("tcp-"), true);
store.dispatch({ type: "RUN" }); store.dispatch({ type: "RUN" });
await waitForTaskHalCommand(store); await waitForTaskHalCommand(store);

View File

@@ -245,6 +245,7 @@ function startFrameCapture(page) {
mode: summary.mode, mode: summary.mode,
interpState: summary.interpState, interpState: summary.interpState,
taskPaused: summary.taskPaused, taskPaused: summary.taskPaused,
motionPaused: summary.motionPaused,
activeLine: summary.activeLine, activeLine: summary.activeLine,
sampleIndex: summary.programExecutionSampleIndex, sampleIndex: summary.programExecutionSampleIndex,
sourceFile: summary.programUiExecution?.sourceFile || null, sourceFile: summary.programUiExecution?.sourceFile || null,
@@ -402,6 +403,7 @@ async function readStateSummary(page) {
interpState: state.machine?.interpState, interpState: state.machine?.interpState,
interpResumeState: state.machine?.interpResumeState, interpResumeState: state.machine?.interpResumeState,
taskPaused: state.machine?.taskPaused, taskPaused: state.machine?.taskPaused,
motionPaused: state.machine?.motionPaused,
rtcpState: state.rtcpState, rtcpState: state.rtcpState,
kinsType: state.kinsType, kinsType: state.kinsType,
activeLine: state.activeLine, activeLine: state.activeLine,
@@ -476,6 +478,7 @@ function summarizeForEvent(state) {
mode: state?.machine?.mode || state?.mode, mode: state?.machine?.mode || state?.mode,
interpState: state?.machine?.interpState || state?.interpState, interpState: state?.machine?.interpState || state?.interpState,
taskPaused: state?.machine?.taskPaused ?? state?.taskPaused, taskPaused: state?.machine?.taskPaused ?? state?.taskPaused,
motionPaused: state?.machine?.motionPaused ?? state?.motionPaused,
activeLine: state?.activeLine, activeLine: state?.activeLine,
sampleIndex: state?.programExecutionSampleIndex, sampleIndex: state?.programExecutionSampleIndex,
programExecutionSourceMode: state?.programExecutionSourceMode, programExecutionSourceMode: state?.programExecutionSourceMode,

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