Align Web execute-next-line with LinuxCNC
This commit is contained in:
@@ -219,8 +219,53 @@ command are blocked instead of being silently mapped to another operation.
|
||||
Step 从第 0 行启动并暂停;PAUSED 且有运动队列时调用 `emcTrajStep`,
|
||||
按执行 ID 推进并再次暂停;无排队运动时释放下一条 task 指令。
|
||||
`motion/control.c:2186` 在执行 ID 改变后暂停 TP,不能“解析一行后永久
|
||||
冻结”,也不能一次 Step 把全部已排队运动执行完。Web 单步的源码行解析
|
||||
与原生 NML 指令细分仍是需单独验证的适配边界,不能用行数相同替代运动断言。
|
||||
冻结”,也不能一次 Step 把全部已排队运动执行完。Web 已将该边界映射为
|
||||
`step_phase`、`step_source_line`、`step_target_id` 和
|
||||
`step_waiting_for_motion`,并用 WASM execution-id/pose 断言,不以行数相同
|
||||
替代运动完成。
|
||||
|
||||
---
|
||||
|
||||
### Execute-next-line 流程(源码复核 2026-09-10)
|
||||
|
||||
原生入口由 `axis.py:2354-2359` 的 `task_step()` 提供,工具栏
|
||||
`axis.tcl:535-541` 和快捷键 `T` 都调用同一个函数。处理顺序固定为:
|
||||
|
||||
```text
|
||||
T / toolbar Step
|
||||
-> 清除编辑器高亮;若不是 AUTO+IDLE,执行 run_warn(),用户取消则结束
|
||||
-> ensure_mode(AUTO)
|
||||
-> c.auto(AUTO_STEP)
|
||||
-> emcmodule.cc 将 AUTO_STEP 转为 EMC_TASK_PLAN_STEP_TYPE
|
||||
-> emctaskmain 根据 interpState 选择 IDLE / READING / PAUSED / WAITING 分支
|
||||
-> task/TP 在后续周期执行一个源码边界或一个当前 execution-id
|
||||
-> motion/control.c 在 execution-id 改变时暂停 TP
|
||||
-> 发布 current_line、execution_line、pose、joints、queue、stepping
|
||||
```
|
||||
|
||||
| 分支 | 原生动作 | Web/WASM 动作 | 响应要求 |
|
||||
|---|---|---|---|
|
||||
| IDLE 首次 | `AUTO_RUN(line=0)`,随即 `emcTrajPause()`,保存恢复状态 | `WasmProgramRuntime.start()` 后立即 `pause()`;任务从 READY 进入 RUNNING 再进入 PAUSED | `step_phase=start_pause`,源码行 0,不产生运动 |
|
||||
| READING | `stepping=1`、`steppingWait=0`,继续 readahead | 只解析一个 WASM canonical source block;有运动时入 TP 并调用 `stepLine()` | `step_phase=source_line_motion` 或 `source_line_pause`,返回 source line 和 `program_steps` |
|
||||
| READING/WAITING 或 PAUSED + queue>0 | `stepping=1`;PAUSED 时调用 `emcTrajStep()` | 运行中的 interpreter 先保存为 PAUSED,再按当前 `active_id` 调用 `sim_linuxcnc_tp_step_line()` | `step_phase=motion_step`,`step_target_id` 固定,等待后续 SIM_STEP 到 execution-id 改变 |
|
||||
| PAUSED + queue=0 | 释放 `interpResumeState`,保持 task_paused | 恢复 WASM interpreter,执行一个 source block,再暂停 | 不能重读已执行行;状态仍为 PAUSED |
|
||||
| WAITING | 设置 stepping,等待队列/输入完成 | 保留 WAITING 的 program status;只推进尚未完成的 TP 或返回 terminal | 不能把 WAITING 改写成 READING |
|
||||
| COMPLETED/ABORTED + queue=0 | task plan 可重新从 line 0 运行 | 先重载当前 source 到 loaded、恢复 STOPPED task,再执行 IDLE 首次分支 | `reopened_taskfile=true`,不得触发 STOPPED->RUNNING 非法转换 |
|
||||
| stepping 已进行 | 原生 GUI 不应叠加新的运动步 | Web 重复请求返回幂等观察结果,不重复入队 | `step_phase=motion_step_pending`,保留原 `step_target_id` |
|
||||
|
||||
Web 的 `ui_gates.step` 必须保持源码公式 `STATE_ON && taskfile`,不能增加
|
||||
IDLE-only 条件;WASM task gate 仍负责机器状态、回零、程序存在和 AUTO 模式。
|
||||
Step 命令本身不推进 logical tick,运动只由后续 `SIM_STEP`/servo 周期推进。
|
||||
一个 source block 产生的 canonical motion 必须在同一 Step 事务中进入 TP 并
|
||||
进入 stepping;前端收到 `step_waiting_for_motion=true` 后启动已有的采样定时器。
|
||||
命令结果必须包含 `program`、`task`、`wasm_motion`、`motion`、
|
||||
`step_phase`、`step_source_line` 和 `step_target_id`(无目标时为 `null`),
|
||||
以便程序行、执行 ID、轨迹和机床姿态可以共同审计。错误只返回稳定的
|
||||
`axis_gate`、`program_step`、`motion_queue` 或 interpreter 错误,不改变已有
|
||||
程序、TP 或 live-path epoch。
|
||||
|
||||
---
|
||||
|
||||
5. **回零采用原生 homemod。** `switchkins.c::kinematicsType()` 始终返回
|
||||
`KINEMATICS_BOTH`,M429 不改变这一类型。世界轴字母不能作为回零目标;
|
||||
`manual-home-joint-select` 明确选择数字关节,`manual-home-axis` 保留 ID,
|
||||
@@ -345,7 +390,7 @@ safety adaptation, not attributed to AXIS.
|
||||
| Run | `PROGRAM_RUN` | `run_warn`, `ensure_mode(AUTO)`, `AUTO_RUN` from the selected start line, then periodic task/TP ticks. Normal AXIS Run uses line 0. In `emctaskmain.cc`, an ended or aborted task plan is closed; the next `EMC_TASK_PLAN_RUN` reopens the current taskfile and runs from line 0. Therefore Run after `completed` or `aborted` must work directly and must not require a separate Reload/Restart click. |
|
||||
| Run from selected line | not exposed | AXIS `task_run_line` requires an editor highlight and source `AUTO_RUN` start line. The Web editor has no source-line selection command yet, so no partial/fake button is rendered. |
|
||||
| Reverse / Forward | not exposed | AXIS `task_reverse`/`task_forward` are keyboard-only source actions; the selected WASM program ABI has no corresponding command and Web preserves the distinction from Run. |
|
||||
| Step | `PROGRAM_STEP` | `SOURCE_ADAPTER`: source toolbar gate is ON plus taskfile, not interpreter IDLE. `task_step` clears highlight/runs warning unless already AUTO+IDLE, ensures AUTO and sends `AUTO_STEP`. Task-layer homing remains authoritative. |
|
||||
| Step | `PROGRAM_STEP` | `SOURCE_ADAPTER`: source toolbar gate is ON plus taskfile, not interpreter IDLE. `task_step` clears highlight/runs warning unless already AUTO+IDLE, ensures AUTO and sends `AUTO_STEP`. Worker publishes `step_phase` (`start_pause`, `source_line_pause`, `source_line_motion`, `motion_step`, `terminal`) with source line, execution ID, program/task/motion state. Task-layer homing remains authoritative. |
|
||||
| Pause / Resume | `PROGRAM_PAUSE` / `PROGRAM_RESUME` | `SOURCE_ADAPTER`: `task_pause` accepts AUTO plus READING/WAITING. `task_resume` requires `paused` plus mode AUTO or MDI. The combined toolbar calls `task_pauseresume`, accepts AUTO or MDI, blocks resume when `resume_inhibit`, and otherwise pauses any non-IDLE interpreter. Web scheduling must serialize the edge but cannot change these state conditions. |
|
||||
| Stop | `PROGRAM_ABORT` | AXIS enables Stop only while the interpreter is non-idle; then it highlights the active line, issues `c.abort()`, waits for completion, and clears the abort edge. The Worker may accept an internal abort edge in more states, but the Web button follows the AXIS UI gate. |
|
||||
| Block delete / optional pause | disabled | LinuxCNC source has these controls, but the selected WASM ABI does not expose their canonical state. They are visible as disabled, with `blocked_by_abi`, and never emulate a local toggle. |
|
||||
@@ -1000,6 +1045,9 @@ predicates (7488 assertions), rather than comparing two rewritten formulas.
|
||||
motion Step, WAITING/MDI Pause-Resume, source OFF state, KINEMATICS_BOTH
|
||||
world-axis rejection and spindle override 0-to-100 restoration using
|
||||
the actual WASM runtime.
|
||||
`web/worker/test_xyzac_step_flow.mjs` verifies the source-aligned Step phases,
|
||||
immediate motion stepping, execution-id boundary completion, duplicate-step
|
||||
idempotence, and completed/aborted task-plan reopen from line zero.
|
||||
The exhaustive control-ID audit is enforced by
|
||||
`scripts/test_axis_control_inventory.mjs` and
|
||||
`scripts/test_axis_control_inventory_browser.mjs`; the latter rejects any
|
||||
|
||||
@@ -181,10 +181,11 @@ try {
|
||||
await clickAck("toolbar-step", "PROGRAM_STEP");
|
||||
assert.equal((await pose()).x, x);
|
||||
await clickAck("toolbar-step", "PROGRAM_STEP");
|
||||
await clickAck("toolbar-step", "PROGRAM_STEP");
|
||||
await clickAck("toolbar-step", "PROGRAM_STEP"); await queueEmpty();
|
||||
await wait(() => Math.abs(JSON.parse(document.querySelector('[data-xyzac-preview="toolpath"]').dataset.xyzacLivePose).x - 2) < 1e-7, "step_first_motion_pose");
|
||||
assert.ok(Math.abs((await pose()).x - 2) < 1e-7);
|
||||
await clickAck("toolbar-step", "PROGRAM_STEP"); await clickAck("toolbar-step", "PROGRAM_STEP"); await queueEmpty();
|
||||
await clickAck("toolbar-step", "PROGRAM_STEP"); await queueEmpty();
|
||||
await wait(() => Math.abs(JSON.parse(document.querySelector('[data-xyzac-preview="toolpath"]').dataset.xyzacLivePose).x - 3) < 1e-7, "step_second_motion_pose");
|
||||
assert.ok(Math.abs((await pose()).x - 3) < 1e-7);
|
||||
await clickAck("toolbar-stop", "PROGRAM_ABORT");
|
||||
await wait(() => document.querySelector("main")?.dataset.axisTaskMode === "MANUAL");
|
||||
|
||||
@@ -395,8 +395,9 @@ export function App() {
|
||||
setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`);
|
||||
if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer();
|
||||
if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer();
|
||||
const result = (operation.response as { payload?: { homing?: { phase?: string }; panel_snapshot?: { motion?: { homing?: boolean } } } }).payload;
|
||||
const result = (operation.response as { payload?: { homing?: { phase?: string }; step_waiting_for_motion?: boolean; panel_snapshot?: { motion?: { homing?: boolean } } } }).payload;
|
||||
if (kind === "HOME" && result?.homing?.phase === "accepted") startHomeTimer();
|
||||
if (kind === "PROGRAM_STEP" && result?.step_waiting_for_motion === true) startRunTimer();
|
||||
// Ordinary abort keeps sampling the native free_tp deceleration.
|
||||
if (kind === "PROGRAM_ABORT") { if (result?.panel_snapshot?.motion?.homing) startHomeTimer(); else stopHomeTimer(); }
|
||||
if (["ESTOP", "POWER"].includes(kind)) stopHomeTimer();
|
||||
|
||||
@@ -145,6 +145,7 @@ export class SimSession {
|
||||
this.pendingProgramControl = 0;
|
||||
this.pendingTaskSync = 0;
|
||||
this.pendingXyzacMotion = null;
|
||||
this.xyzacStepTargetId = 0;
|
||||
this.pendingServoSync = 0;
|
||||
this.xyzacExecutionWaitNextMotion = false;
|
||||
this.xyzacExecutionWaitSamples = 0;
|
||||
@@ -2322,20 +2323,68 @@ export class SimSession {
|
||||
case "PROGRAM_STEP": {
|
||||
if (this.xyzacProgram) {
|
||||
try {
|
||||
const current = this.xyzacProgram.snapshot();
|
||||
const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state);
|
||||
let current = this.xyzacProgram.snapshot();
|
||||
let queueDepth = Number(this.wasmMotion.read().state?.queue_depth ?? 0);
|
||||
let stepPhase = "source_line_pause";
|
||||
let stepWaitingForMotion = false;
|
||||
let stepSourceLine = current.execution_source_line ?? current.current_line ?? 0;
|
||||
let stepTargetId = null;
|
||||
let stepResult = null;
|
||||
let stepTrace = [];
|
||||
this.task.setMode("AUTO");
|
||||
this.wasmHomeLimit.setMode("COORD");
|
||||
if (this.wasmMotion.isStepping()) {
|
||||
const motion = this.wasmMotion.read();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "motion_step_pending", step_waiting_for_motion: true, step_target_id: Number(motion.state.active_id ?? 0) || this.xyzacStepTargetId || null, program_steps: [], execution_trace: [], program: current, task: this.task.export(), wasm_motion: motion, motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (queueDepth > 0) {
|
||||
const targetId = Number(this.wasmMotion.read().state.active_id ?? 0) || this.xyzacStepTargetId;
|
||||
this.xyzacStepTargetId = targetId;
|
||||
if (current.status === 2) {
|
||||
const paused = this.xyzacProgram.pause();
|
||||
if (paused.status !== "verified") return finish(this.error(input, "program_pause", paused.code), "failed");
|
||||
if (this.task.state.state === "READY") this.task.transition("RUNNING");
|
||||
if (this.task.state.state === "RUNNING") this.task.transition("PAUSED");
|
||||
this.task.setProgram("paused", current.current_line);
|
||||
current = this.xyzacProgram.snapshot();
|
||||
}
|
||||
this.wasmMotion.stepLine();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "motion_step", step_waiting_for_motion: true, step_target_id: targetId || null, step_source_line: current.execution_source_line ?? current.current_line, program_steps: [], execution_trace: [], program: current, task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (["completed", "aborted"].includes(current.program_status) && this.task.state.state === "STOPPED") {
|
||||
const reset = this.resetXyzacProgramToLoaded();
|
||||
if (reset.status !== "verified") return finish(this.error(input, "program_step", reset.code ?? "program_reload_failed"), "failed");
|
||||
this.wasmMotion.setPaused(false);
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
if (typeof this.wasmMotion.setActiveKinematicsTag === "function") this.wasmMotion.setActiveKinematicsTag(XYZAC_MODES.identity);
|
||||
this.task.recover();
|
||||
this.task.setProgram("loaded", 0);
|
||||
current = this.xyzacProgram.snapshot();
|
||||
}
|
||||
const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state) && ![2, 3].includes(current.status);
|
||||
if (idle) {
|
||||
const started = this.xyzacProgram.start();
|
||||
if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed");
|
||||
} else if (current.status === 3 && this.wasmMotion.queueDepth() > 0) {
|
||||
this.wasmMotion.stepLine();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, program: current, task: this.task.export(), motion: this.wasmMotionProjection() }), "verified");
|
||||
stepPhase = "start_pause";
|
||||
stepSourceLine = 0;
|
||||
} else {
|
||||
if (current.status === 3) this.xyzacProgram.resume();
|
||||
if (current.status === 3) {
|
||||
const resumed = this.xyzacProgram.resume();
|
||||
if (resumed.status !== "verified") return finish(this.error(input, "program_resume", resumed.code), "failed");
|
||||
current = this.xyzacProgram.snapshot();
|
||||
if (current.status === 4) {
|
||||
this.task.setProgram("completed", current.current_line);
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "terminal", step_waiting_for_motion: false, step_source_line: current.current_line, program_steps: [], execution_trace: [], program: current, task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (current.status === 1) {
|
||||
const started = this.xyzacProgram.start();
|
||||
if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed");
|
||||
}
|
||||
}
|
||||
const stepped = this.xyzacProgram.step();
|
||||
if (stepped.status !== "verified") return finish(this.error(input, "program_step", stepped.code), "failed");
|
||||
stepResult = stepped;
|
||||
const sync = this.syncXyzacProgramSpindle(stepped.program);
|
||||
if (sync.status !== "verified" && sync.status !== "skipped") return finish(this.error(input, "program_spindle", sync.code), "failed");
|
||||
if (stepped.has_control && [428, 429, 430].includes(stepped.control_code)) {
|
||||
@@ -2346,20 +2395,30 @@ export class SimSession {
|
||||
stepped.motion.kinematics_mode = this.xyzacProgramKinematicsMode;
|
||||
const queued = this.wasmMotion.enqueue(stepped.motion);
|
||||
if (queued.status === "error") return finish(this.error(input, "motion_queue", queued.code), "failed");
|
||||
// AUTO_STEP releases the current trajectory line. The WASM TP
|
||||
// will stop again when its execution ID changes.
|
||||
this.wasmMotion.stepLine();
|
||||
stepTargetId = Number(stepped.motion.source_line ?? stepped.line ?? 0) || Number(this.wasmMotion.read().state.active_id ?? 0) || null;
|
||||
this.xyzacStepTargetId = stepTargetId ?? 0;
|
||||
stepPhase = "source_line_motion";
|
||||
stepWaitingForMotion = true;
|
||||
}
|
||||
stepSourceLine = stepped.program?.execution_source_line ?? stepped.line ?? stepSourceLine;
|
||||
stepTrace = [{ line: stepSourceLine, text: stepped.program?.execution_line_text ?? "", program_status: stepped.program?.program_status ?? null }].filter((entry) => Number.isSafeInteger(entry.line) && entry.line > 0);
|
||||
}
|
||||
if (this.xyzacProgram.snapshot().status === 4 && this.wasmMotion.read().state.queue_depth === 0) {
|
||||
this.task.setProgram("completed", this.xyzacProgram.snapshot().current_line);
|
||||
if (this.task.state.state !== "STOPPED") this.task.transition("STOPPED");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion: this.wasmMotionProjection() }), "verified");
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "terminal", step_waiting_for_motion: false, step_source_line: this.xyzacProgram.snapshot().current_line, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
const paused = this.xyzacProgram.pause();
|
||||
if (paused.status !== "verified") return finish(this.error(input, "program_pause", paused.code), "failed");
|
||||
this.wasmMotion.setPaused(true);
|
||||
if (["READY", "STOPPED"].includes(this.task.state.state)) this.task.transition("RUNNING");
|
||||
if (!this.wasmMotion.isStepping()) this.wasmMotion.setPaused(true);
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
if (this.task.state.state === "READY") this.task.transition("RUNNING");
|
||||
if (this.task.state.state === "RUNNING") this.task.transition("PAUSED");
|
||||
this.task.setProgram("paused", this.xyzacProgram.snapshot().current_line);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion: this.wasmMotionProjection() }), "verified");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: stepPhase, step_waiting_for_motion: stepWaitingForMotion || this.wasmMotion.isStepping(), step_source_line: stepSourceLine, step_target_id: stepTargetId, program_steps: stepResult ? [stepResult] : [], execution_trace: stepTrace, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "program_step", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
|
||||
100
web/worker/test_xyzac_step_flow.mjs
Normal file
100
web/worker/test_xyzac_step_flow.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
||||
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
|
||||
|
||||
const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
||||
const fetchImpl = async (url) => {
|
||||
try {
|
||||
const body = await readFile(String(url).replace(/^\//, ""), "utf8");
|
||||
return { ok: true, text: async () => body };
|
||||
} catch {
|
||||
return { ok: false, text: async () => "" };
|
||||
}
|
||||
};
|
||||
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
||||
assert.equal(bundle.status, "ready");
|
||||
const core = await createModule({ wasmBinary: await readFile("./build-wasm/linuxcnc-sim-core.wasm") });
|
||||
const session = new SimSession(core, { sessionId: "xyzac-step-flow" });
|
||||
let seq = 0;
|
||||
const send = (kind, payload = {}) => session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, seq: ++seq, operation_id: `step-flow-${seq}`, kind, payload });
|
||||
const payload = (kind, body = {}) => {
|
||||
const response = send(kind, body);
|
||||
assert.notEqual(response.kind, "ERROR", `${kind}: ${response.payload?.gate_reason ?? response.payload?.message_key ?? response.payload?.code}`);
|
||||
return response.payload;
|
||||
};
|
||||
const step = () => payload("PROGRAM_STEP");
|
||||
const drain = () => {
|
||||
let cycles = 0;
|
||||
while (session.wasmMotion.isStepping() && cycles < 1000) {
|
||||
payload("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
cycles += 1;
|
||||
}
|
||||
assert.equal(session.wasmMotion.isStepping(), false, "single-step motion must reach an execution-id boundary");
|
||||
return cycles;
|
||||
};
|
||||
|
||||
payload("HELLO");
|
||||
payload("SESSION_ATTACH");
|
||||
payload("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
payload("PROGRAM_LOAD", { path: "step-flow.ngc", source: "G21 G90\nG1 X1 F600\nG1 X2 F600\nM2\n" });
|
||||
payload("POWER", { active: true });
|
||||
payload("HOME", { all: true });
|
||||
payload("SIM_STEP", { cycles: 1 });
|
||||
|
||||
let result = step();
|
||||
assert.equal(result.step_phase, "start_pause");
|
||||
assert.equal(result.step_source_line, 0);
|
||||
assert.equal(result.step_waiting_for_motion, false);
|
||||
assert.equal(result.task.state, "PAUSED");
|
||||
|
||||
result = step();
|
||||
assert.equal(result.step_phase, "source_line_pause");
|
||||
assert.equal(result.step_source_line, 1);
|
||||
assert.equal(result.program_steps.length, 1);
|
||||
|
||||
result = step();
|
||||
assert.equal(result.step_phase, "source_line_motion");
|
||||
assert.equal(result.step_waiting_for_motion, true);
|
||||
assert.equal(result.step_source_line, 2);
|
||||
assert.equal(result.program_steps.length, 1);
|
||||
const firstTarget = result.step_target_id ?? result.wasm_motion.state.active_id;
|
||||
assert.ok(Number.isSafeInteger(result.step_target_id) && result.step_target_id > 0);
|
||||
assert.equal(result.motion.stepping, true);
|
||||
|
||||
const duplicate = step();
|
||||
assert.equal(duplicate.step_phase, "motion_step_pending");
|
||||
assert.equal(duplicate.step_target_id, firstTarget);
|
||||
assert.equal(duplicate.program_steps.length, 0);
|
||||
drain();
|
||||
assert.ok(Math.abs(session.wasmMotion.read().state.position.x - 1) < 1e-8);
|
||||
|
||||
result = step();
|
||||
assert.equal(result.step_phase, "source_line_motion");
|
||||
assert.equal(result.step_source_line, 3);
|
||||
drain();
|
||||
assert.ok(Math.abs(session.wasmMotion.read().state.position.x - 2) < 1e-8);
|
||||
|
||||
result = step();
|
||||
assert.equal(result.step_phase, "terminal");
|
||||
assert.equal(result.program.program_status, "completed");
|
||||
assert.equal(result.task.state, "STOPPED");
|
||||
|
||||
result = step();
|
||||
assert.equal(result.step_phase, "start_pause");
|
||||
assert.equal(result.program.program_status, "paused");
|
||||
assert.equal(result.task.state, "PAUSED");
|
||||
payload("PROGRAM_ABORT");
|
||||
result = step();
|
||||
assert.equal(result.step_phase, "start_pause");
|
||||
assert.equal(result.program.program_status, "paused");
|
||||
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-step-flow.v2",
|
||||
status: "verified",
|
||||
source: ["axis.py::task_step", "emctaskmain.cc::EMC_TASK_PLAN_STEP_TYPE", "sim_linuxcnc_tp_step_line"],
|
||||
phases: ["start_pause", "source_line_pause", "source_line_motion", "motion_step_pending", "terminal"],
|
||||
duplicate_step_idempotent: true,
|
||||
completed_and_aborted_rerun: true,
|
||||
}));
|
||||
Reference in New Issue
Block a user