From 1342cff3f0c4545c570567e8492f1184a76d5467 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Tue, 1 Sep 2026 21:12:52 +0800 Subject: [PATCH] Align pause resume and abort lifecycle --- wasm-core/src/sim_protocol.c | 3 ++ wasm-core/tests/sim_protocol_test.c | 15 +++--- web/react/src/App.tsx | 7 ++- web/worker/sim-session.mjs | 52 ++++++++++++-------- web/worker/test_xyzac_button_constraints.mjs | 7 +++ 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/wasm-core/src/sim_protocol.c b/wasm-core/src/sim_protocol.c index 2470271..eac4acd 100644 --- a/wasm-core/src/sim_protocol.c +++ b/wasm-core/src/sim_protocol.c @@ -47,6 +47,8 @@ static int state_accepts_kind(uint32_t current, const char *kind) { if (current == SIM_PROTOCOL_READY) return 1; if (current == SIM_PROTOCOL_PAUSED) { return is_kind(kind, "PROGRAM_RESUME") || is_kind(kind, "RESUME") || + is_kind(kind, "PROGRAM_ABORT") || is_kind(kind, "ESTOP") || + is_kind(kind, "POWER") || is_kind(kind, "SET_OVERRIDE") || is_kind(kind, "PING") || is_kind(kind, "SNAPSHOT_REQUEST") || is_kind(kind, "DESTROY"); } @@ -76,6 +78,7 @@ int32_t sim_protocol_accept(uint32_t schema_version, uint32_t seq, else if (is_kind(kind, "SESSION_READY")) state.state = SIM_PROTOCOL_READY; else if (is_kind(kind, "PROGRAM_PAUSE")) state.state = SIM_PROTOCOL_PAUSED; else if (is_kind(kind, "PROGRAM_RESUME") || is_kind(kind, "RESUME")) state.state = SIM_PROTOCOL_READY; + else if (is_kind(kind, "PROGRAM_ABORT") || is_kind(kind, "ESTOP") || is_kind(kind, "POWER")) state.state = SIM_PROTOCOL_READY; else if (is_kind(kind, "DESTROY")) state.state = SIM_PROTOCOL_DESTROYED; else if (is_kind(kind, "RESET")) { /* RESET is a machine/task reset once attached. Keep the transport diff --git a/wasm-core/tests/sim_protocol_test.c b/wasm-core/tests/sim_protocol_test.c index 98c2040..e11093d 100644 --- a/wasm-core/tests/sim_protocol_test.c +++ b/wasm-core/tests/sim_protocol_test.c @@ -15,12 +15,15 @@ int main(void) { assert(sim_protocol_last_seq() == 4); assert(sim_protocol_accept(1, 5, "PROGRAM_PAUSE", "op-4", 2) == SIM_PROTOCOL_OK); assert(sim_protocol_accept(1, 6, "MDI_EXECUTE", "op-5", 2) == SIM_PROTOCOL_ERR_STATE); - assert(sim_protocol_accept(1, 6, "PROGRAM_RESUME", "op-5", 2) == SIM_PROTOCOL_OK); - assert(sim_protocol_accept(2, 7, "PING", NULL, 2) == SIM_PROTOCOL_ERR_SCHEMA); - assert(sim_protocol_accept(1, 7, "PING", NULL, 1) == SIM_PROTOCOL_ERR_TICK); - assert(sim_protocol_accept(1, 7, "DESTROY", NULL, 2) == SIM_PROTOCOL_OK); - assert(sim_protocol_accept(1, 8, "PING", NULL, 2) == SIM_PROTOCOL_ERR_STATE); - assert(sim_protocol_accept(1, 9, "RESET", "reset-1", 0) == SIM_PROTOCOL_OK); + assert(sim_protocol_accept(1, 6, "PROGRAM_ABORT", "op-5", 2) == SIM_PROTOCOL_OK); + assert(sim_protocol_state_value() == SIM_PROTOCOL_READY); + assert(sim_protocol_accept(1, 7, "PROGRAM_PAUSE", "op-6", 2) == SIM_PROTOCOL_OK); + assert(sim_protocol_accept(1, 8, "PROGRAM_RESUME", "op-7", 2) == SIM_PROTOCOL_OK); + assert(sim_protocol_accept(2, 9, "PING", NULL, 2) == SIM_PROTOCOL_ERR_SCHEMA); + assert(sim_protocol_accept(1, 9, "PING", NULL, 1) == SIM_PROTOCOL_ERR_TICK); + assert(sim_protocol_accept(1, 9, "DESTROY", NULL, 2) == SIM_PROTOCOL_OK); + assert(sim_protocol_accept(1, 10, "PING", NULL, 2) == SIM_PROTOCOL_ERR_STATE); + assert(sim_protocol_accept(1, 11, "RESET", "reset-1", 0) == SIM_PROTOCOL_OK); assert(sim_protocol_state_value() == SIM_PROTOCOL_NEW); assert(sim_protocol_set_tick(4) == SIM_PROTOCOL_ERR_TICK); return 0; diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 3309d8c..a39e6dc 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -219,10 +219,15 @@ export function App() { const startRunTimer = () => { if (runTimer.current) return; runTimer.current = setInterval(() => { if (runStepPending.current) return; runStepPending.current = true; operationController.execute("SIM_STEP", { cycles: 5, response: "motion_delta" }, { state: axisState, gate: false }).catch(() => undefined).finally(() => { runStepPending.current = false; }); }, 50); }; const dispatch = (kind: string, payload: Record = {}, gate = true, action: string | null = null): Promise => { setCommandStatus(`${kind} pending`); + // The AXIS task stops issuing servo/task polls before changing the + // interpreter state. A SIM_STEP already in flight is allowed to finish, + // but no new poll may race PROGRAM_PAUSE/PROGRAM_ABORT and consume the + // protocol sequence reserved for RESUME. if (kind === "JOG_STOP") stopJogTimer(); + if (["PROGRAM_PAUSE", "PROGRAM_ABORT"].includes(kind)) stopRunTimer(); const pending = operationController.execute(kind, payload, { state: axisState, gate, ...(action ? { action } : {}) }); setOperationTrace(operationController.exportTrace()); - return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN") startRunTimer(); if (kind === "PROGRAM_ABORT" || kind === "PROGRAM_RESTART") stopRunTimer(); if (kind === "JOG_START") startJogTimer(); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => { + return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "JOG_START") startJogTimer(); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); const code = error.message; setCommandStatus(`${kind} blocked: ${code}`); diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index bce9478..0487df7 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -1862,7 +1862,7 @@ export class SimSession { if (this.xyzacProgram) { const result = this.xyzacProgram.start(); if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); } - return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); + return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); } const result = this.program.start(); if (result.status === "verified") { @@ -1885,30 +1885,40 @@ export class SimSession { case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": { const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort"; if (this.xyzacProgram) { - this.pendingXyzacMotion = null; - this.pendingProgramControl = 0; - this.pendingTaskSync = 0; - this.pendingServoSync = 0; - this.xyzacExecutionWaitNextMotion = false; - this.xyzacExecutionWaitSamples = 0; - this.xyzacLastEnqueuedKinematicsMode = 0; - this.xyzacDeferredPhysicalSwitchMode = null; - this.xyzacDeferredBlockedActiveId = 0; - this.xyzacDeferredControlMotionType = 0; - this.xyzacQueuedLookaheadTargetId = 0; - this.xyzacAllowQueuedLookahead = false; - this.xyzacLastServoActiveId = 0; - this.xyzacLastQueuedMotion = null; - this.xyzacDeferredPhysicalPose = null; - this.xyzacDeferredPhysicalPoseApplied = false; - this.xyzacAppendDeferredOnActive = false; + // PAUSE/RESUME are interpreter and TP state transitions. They must + // preserve the pending canonical block and queued lookahead so a + // resume continues the exact WASM trajectory. ABORT is the only + // edge that clears the task/TP handoff state. + if (action === "abort") { + this.pendingXyzacMotion = null; + this.pendingProgramControl = 0; + this.pendingTaskSync = 0; + this.pendingServoSync = 0; + this.xyzacExecutionWaitNextMotion = false; + this.xyzacExecutionWaitSamples = 0; + this.xyzacLastEnqueuedKinematicsMode = 0; + this.xyzacDeferredPhysicalSwitchMode = null; + this.xyzacDeferredBlockedActiveId = 0; + this.xyzacDeferredControlMotionType = 0; + this.xyzacQueuedLookaheadTargetId = 0; + this.xyzacAllowQueuedLookahead = false; + this.xyzacLastServoActiveId = 0; + this.xyzacLastQueuedMotion = null; + this.xyzacDeferredPhysicalPose = null; + this.xyzacDeferredPhysicalPoseApplied = false; + this.xyzacAppendDeferredOnActive = false; + } const result = this.xyzacProgram[action](); if (result.status === "verified") { - if (action === "pause") { call(this.core, "sim_protocol_pause"); this.wasmMotion.setPaused(true); this.task.transition("PAUSED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("paused", program.execution_source_line ?? program.current_line); } - else if (action === "resume") { call(this.core, "sim_protocol_resume"); this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); } + // sim_protocol_accept has already applied the transport edge; + // calling sim_protocol_pause/resume a second time would return a + // false ERR_STATE. The LinuxCNC WASM TP remains authoritative for + // the actual pause/resume motion transition. + if (action === "pause") { this.wasmMotion.setPaused(true); this.task.transition("PAUSED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("paused", program.execution_source_line ?? program.current_line); } + else if (action === "resume") { this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); } else { this.wasmMotion.abort(); this.task.transition("STOPPED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("aborted", program.execution_source_line ?? program.current_line); } } - return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); + return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); } const result = this.program[action](); if (result.status === "verified") { diff --git a/web/worker/test_xyzac_button_constraints.mjs b/web/worker/test_xyzac_button_constraints.mjs index 7fc0e46..d1d321d 100644 --- a/web/worker/test_xyzac_button_constraints.mjs +++ b/web/worker/test_xyzac_button_constraints.mjs @@ -50,6 +50,13 @@ for (const [kind, payload, reason] of [["HOME", { all: true }, "xyzac_interp_run assert.equal(blocked.payload.gate_reason, reason, kind); } assert.equal(send("PROGRAM_PAUSE").payload.task.state, "PAUSED"); +const abortedWhilePaused = send("PROGRAM_ABORT"); +assert.equal(abortedWhilePaused.payload.task.state, "STOPPED"); +assert.equal(abortedWhilePaused.payload.task.program_status, "aborted"); +assert.equal(abortedWhilePaused.payload.wasm_motion.state.queue_depth, 0); +assert.equal(send("PROGRAM_RESTART").payload.task.program_status, "loaded"); +assert.equal(send("PROGRAM_RUN").payload.task.state, "RUNNING"); +assert.equal(send("PROGRAM_PAUSE").payload.task.state, "PAUSED"); assert.equal(send("PROGRAM_RESUME").payload.task.state, "RUNNING"); assert.equal(send("POWER", { active: false }).payload.task.state, "STOPPED"); assert.equal(send("POWER", { active: true }).payload.panel_snapshot.axis_state.machine_state, "ON");