Run XYZAC through upstream LinuxCNC TP

This commit is contained in:
2026-09-01 11:44:05 +08:00
parent bbdfad8364
commit d18141ec81
23 changed files with 1550 additions and 139 deletions

View File

@@ -22,6 +22,7 @@ import { ConfigurationBatchRuntime } from "../workspace/configuration-batch.mjs"
import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
import { axisState } from "./axis-state.mjs";
import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
import { WasmLinuxcncTp } from "./wasm-linuxcnc-tp.mjs";
import { WasmHomeLimit } from "./wasm-home-limit.mjs";
import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs";
import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
@@ -101,10 +102,11 @@ function canonicalPayload(value) {
}
export class SimSession {
constructor(core, { contextId = CONTEXT_ID, sessionId = "" } = {}) {
constructor(core, { contextId = CONTEXT_ID, sessionId = "", planner = "queue" } = {}) {
this.core = core;
this.contextId = contextId;
this.sessionId = sessionId;
this.planner = planner;
this.tick = 0;
this.created = false;
this.timeline = new ReplayTimeline();
@@ -121,7 +123,7 @@ export class SimSession {
this.python = new PythonBridge();
this.program = new ProgramRunner(core);
this.authoritative = new AuthoritativeRuntime();
this.wasmMotion = new WasmMotionQueue(core);
this.wasmMotion = planner === "linuxcnc-tp" ? new WasmLinuxcncTp(core) : new WasmMotionQueue(core);
this.wasmHomeLimit = new WasmHomeLimit(core);
this.xyzacKinematics = null;
this.xyzacIo = null;
@@ -129,7 +131,28 @@ export class SimSession {
this.xyzacProgram = null;
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
this.xyzacRapidVelocity = 35;
this.xyzacMotionCycleTime = 0.01;
this.xyzacTaskCycleTicks = 1;
this.xyzacAxisVelocityLimits = {};
this.xyzacAxisAccelerationLimits = {};
this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
this.pendingXyzacMotion = null;
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;
this.xyzacDeferredCompletionActiveId = 0;
this.motionAuthority = "js";
this.task = new SimTask(core, { contextId, sessionId: sessionId || null });
this.scheduler = new SimScheduler(core);
@@ -149,13 +172,14 @@ export class SimSession {
// WASM pose samples into every 10 ms step response; the frontend retains
// the canonical path from PROGRAM_LOAD/PROGRAM_RESTART.
const lightweightXyzacStep = input.kind === "SIM_STEP" && Boolean(this.xyzacProgram);
const currentMotion = this.wasmMotion.read();
const traceOnlyStep = lightweightXyzacStep && input.payload?.trace_only === true;
const currentMotion = traceOnlyStep && statePayload.wasm_motion ? statePayload.wasm_motion : this.wasmMotion.read();
const completedMotion = program.status === 4 && currentMotion.state?.queue_depth === 0;
// Compact payloads are only valid for successful motion samples. Error
// responses must retain their code/reason instead of being rewritten into
// a delta with undefined fields.
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion;
const panelSnapshot = motionDelta ? null : this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep });
const panelSnapshot = motionDelta || traceOnlyStep ? null : this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep });
const compactProgram = motionDelta ? {
schema: program.schema,
source: program.source,
@@ -208,7 +232,7 @@ export class SimSession {
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
task_authority: responsePayload.task_authority ?? this.task.authority(),
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}),
axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }),
...(traceOnlyStep ? {} : { axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }) }),
},
};
}
@@ -220,10 +244,11 @@ export class SimSession {
const io = this.xyzacIo?.canonical?.();
const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null;
const kinematics = this.xyzacKinematics?.snapshot?.();
const kinematicsJoints = kinematics?.joints?.length === 5 ? [...kinematics.joints] : null;
const plannerJoints = wasmMotion.status === "verified" && Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : null;
const kinematicsJoints = plannerJoints ?? (kinematics?.joints?.length === 5 ? [...kinematics.joints] : null);
const unsigned = {
schema: "sim_motion.v1",
source: "wasm_motion_queue",
source: wasmMotion.state.source ?? "wasm_motion_queue",
tick: wasmMotion.status === "verified" ? wasmMotion.state.tick : this.tick,
axes: { x: position.x, y: position.y, z: position.z },
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
@@ -269,7 +294,7 @@ export class SimSession {
const path = `${basePath.replace(/\/demos$/, "")}/${suffix}`;
const source = this.workspaceFiles[path];
if (typeof source !== "string") return { status: "error", code: "program_subroutine_missing", path };
const added = this.xyzacProgram.addSubroutine(name, source);
const added = this.xyzacProgram.addSubroutine(name, source, path);
if (added.status !== "verified") return added;
}
return { status: "verified", count: rows.length };
@@ -803,7 +828,10 @@ export class SimSession {
let replayHomeLimit = null;
try {
if (replayMotionAuthority === "wasm") {
replayWasmMotion = new WasmMotionQueue(this.core);
replayWasmMotion = new WasmMotionQueue(this.core, {
cycleTime: Number(checkpoint.state.wasm_motion.cycle_time ?? this.xyzacMotionCycleTime),
velocityLimit: Number(checkpoint.state.wasm_motion.velocity_limit ?? 1e9),
});
replayWasmMotion.restoreState(checkpoint.state.wasm_motion);
if (checkpoint.state.wasm_home_limit) {
replayHomeLimit = new WasmHomeLimit(this.core);
@@ -1154,6 +1182,23 @@ export class SimSession {
// interpreter command and trajectory, then leave the taskfile loaded
// for a later AUTO run after ESTOP_RESET and POWER ON.
this.xyzacProgram.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;
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
const resetProgram = this.resetXyzacProgramToLoaded();
@@ -1240,6 +1285,23 @@ export class SimSession {
/* LinuxCNC emcTaskSetState(OFF) aborts motion, clears the
interpreter command and leaves the loaded taskfile intact. */
this.xyzacProgram.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;
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
const resetProgram = this.resetXyzacProgramToLoaded();
@@ -1348,9 +1410,24 @@ export class SimSession {
this.xyzacDeviceIo?.destroy();
this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 });
this.xyzacProgram?.destroy();
this.wasmMotion.destroy();
this.xyzacMotionCycleTime = Number(configuration.kinematics?.servo_cycle_time ?? configuration.kinematics?.cycle_time ?? 0.01);
if (!Number.isFinite(this.xyzacMotionCycleTime) || this.xyzacMotionCycleTime <= 0) throw new Error("wasm_xyzac_cycle_time_invalid");
const taskCycleTime = Number(configuration.kinematics?.task_cycle_time ?? this.xyzacMotionCycleTime);
this.xyzacTaskCycleTicks = Math.max(1, Math.round(taskCycleTime / this.xyzacMotionCycleTime));
this.wasmMotion = this.planner === "linuxcnc-tp"
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300) })
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
this.xyzacProgram = new WasmProgramRuntime(this.core);
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
const angularLimit = Number(configuration.kinematics?.axis_angular_velocity_limit ?? 30);
this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, a: angularLimit, c: angularLimit };
const accelerationLimit = Number(configuration.kinematics?.max_acceleration ?? 300);
this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, a: accelerationLimit, c: accelerationLimit };
this.wasmMotion.setDynamics(Number(configuration.kinematics?.max_acceleration ?? 300), Number(configuration.kinematics?.max_jerk ?? 1000000000));
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
}
if (configuration?.home_limits && this.motionAuthority === "wasm") {
@@ -1414,54 +1491,152 @@ export class SimSession {
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
let program_steps = [];
const motion_trace = [];
let sourceLookaheadReady = false;
try {
this.scheduler.step(cycles, {
task: () => {
task: (nextTick) => {
if (this.xyzacProgram) {
const wasmState = this.xyzacProgram.snapshot();
if (![2, 3].includes(wasmState.status)) return;
if (this.pendingTaskSync !== 0) {
this.pendingTaskSync = 0;
return;
}
// LinuxCNC's task/interpreter blocks on a full trajectory
// planner queue. Let the motion phase consume one segment
// before reading another source line instead of converting
// normal planner backpressure into a program fault.
if (this.wasmMotion.read().state.queue_depth >= this.wasmMotion.queueCapacity) return;
if (this.pendingXyzacMotion) {
const pendingMode = Number(this.pendingXyzacMotion.kinematics_mode ?? 0);
if (this.xyzacAppendDeferredOnActive) return;
const queueState = this.wasmMotion.read().state;
const deferredControlReady = this.xyzacDeferredControlMotionType === 1
? Number(queueState.active_id ?? 0) !== this.xyzacDeferredBlockedActiveId
: Number(queueState.active_id ?? 0) === this.xyzacDeferredBlockedActiveId;
const deferredSwitchMotion = this.xyzacDeferredPhysicalSwitchMode !== null && pendingMode === this.xyzacDeferredPhysicalSwitchMode && this.xyzacQueuedLookaheadTargetId === 0 && deferredControlReady;
const queuedLookaheadReady = deferredSwitchMotion || (this.xyzacAllowQueuedLookahead && this.xyzacLastServoActiveId === this.xyzacQueuedLookaheadTargetId);
if (queueState.queue_depth !== 0 && !queuedLookaheadReady) return;
if (queuedLookaheadReady && !deferredSwitchMotion && this.xyzacAllowQueuedLookahead) {
// The next segment is appended after the first servo
// sample of the now-active target, matching LinuxCNC's
// TP queue observation (q=1 on entry, q=2 thereafter).
sourceLookaheadReady = true;
return;
}
const pending = this.pendingXyzacMotion;
this.pendingXyzacMotion = null;
const enqueue = this.wasmMotion.enqueue(pending);
if (enqueue.status === "error") throw new Error(enqueue.code);
const modeChanged = pendingMode !== this.xyzacLastEnqueuedKinematicsMode;
this.xyzacLastEnqueuedKinematicsMode = pendingMode;
this.xyzacLastQueuedMotion = pending;
if (this.planner === "linuxcnc-tp" && modeChanged && queueState.queue_depth !== 0) {
this.xyzacAllowQueuedLookahead = true;
this.xyzacQueuedLookaheadTargetId = Number(pending.source_line ?? 0);
}
if (queuedLookaheadReady && !modeChanged) {
this.xyzacAllowQueuedLookahead = false;
this.xyzacQueuedLookaheadTargetId = 0;
}
if (this.xyzacExecutionWaitNextMotion && pendingMode !== XYZAC_MODES.identity) {
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 1;
} else if (pendingMode === XYZAC_MODES.identity) this.xyzacExecutionWaitNextMotion = false;
if (this.planner !== "linuxcnc-tp" || !modeChanged || queueState.queue_depth !== 0) return;
// Source TP performs one servo cycle on the newly active
// segment before task lookahead appends the next XYZAC
// segment. Keep parsing in this task, but defer that queue
// mutation until the first servo sample below.
sourceLookaheadReady = true;
}
// LinuxCNC's task planner consumes non-motion interpreter
// blocks in the same task cycle and stops when the first
// canonical motion is handed to TP. Preserve that boundary
// instead of charging comments, parameter assignments, M429,
// and G10 blocks as artificial motion ticks.
for (let parserStep = 0; parserStep < 64; parserStep += 1) {
if (this.pendingProgramControl !== 0) {
if (this.wasmMotion.read().state.queue_depth !== 0) return;
const current = this.wasmMotion.read().state.position;
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[this.pendingProgramControl];
const selected = this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0]);
if (selected.status !== "verified") throw new Error(`wasm_switchkins_control_${this.pendingProgramControl}`);
this.pendingProgramControl = 0;
return;
}
const step = this.xyzacProgram.step();
if (step.status !== "verified") throw new Error(step.code);
// Path control is produced by the WASM interpreter and applied
// to the WASM trajectory queue before the block's motion is
// enqueued. G61/G61.1 conservatively use the queue's exact
// profile; G64 restores the continuous lookahead profile.
const motionControlMode = Number(step.program?.motion_control_mode ?? 0);
const queueProfile = motionControlMode === 0 ? 0 : 1;
if (this.wasmMotion.controls.profile !== queueProfile) this.wasmMotion.setProfile(queueProfile);
if (step.has_control && [428, 429, 430].includes(step.control_code)) {
this.pendingProgramControl = step.control_code;
if (this.wasmMotion.read().state.queue_depth === 0) {
const current = this.wasmMotion.read().state.position;
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[step.control_code];
const selected = this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0]);
if (selected.status !== "verified") throw new Error(`wasm_switchkins_control_${step.control_code}`);
const pendingControl = this.pendingProgramControl;
const queueState = this.wasmMotion.read().state;
const deferIdentitySwitch = pendingControl === 429 && this.planner === "linuxcnc-tp" && (Number(queueState.motion_type ?? 0) === 3 || (Number(queueState.motion_type ?? 0) === 1 && Number(queueState.progress ?? 0) >= 0.999999));
if (queueState.queue_depth !== 0 && !deferIdentitySwitch) return;
if (queueState.queue_depth !== 0 && deferIdentitySwitch) {
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
this.xyzacDeferredPhysicalSwitchMode = XYZAC_MODES.identity;
this.xyzacDeferredBlockedActiveId = Number(queueState.active_id ?? 0);
this.xyzacDeferredControlMotionType = Number(queueState.motion_type ?? 0);
const queuedEnd = this.xyzacLastQueuedMotion?.end ?? queueState.position;
const endInverse = this.xyzacKinematics.inverseMode(XYZAC_MODES.xyzac, queuedEnd);
if (endInverse.status !== "verified") throw new Error(`wasm_switchkins_deferred_inverse_${endInverse.code ?? "error"}`);
const physical = this.xyzacKinematics.selectPhysical(XYZAC_MODES.identity, endInverse.joints);
if (physical.status !== "verified") throw new Error(`wasm_switchkins_deferred_${physical.code ?? "error"}`);
if (typeof this.wasmMotion.setGoalPosition === "function" && physical.pose) this.wasmMotion.setGoalPosition(physical.pose);
this.xyzacDeferredPhysicalPose = physical.pose;
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacDeferredCompletionActiveId = Number(this.xyzacLastQueuedMotion?.source_line ?? 0);
// Preparing the future identity goal must not switch
// the active kinematics while the helix is running.
const restored = this.xyzacKinematics.selectPhysical(XYZAC_MODES.xyzac, endInverse.joints);
if (restored.status !== "verified") throw new Error(`wasm_switchkins_deferred_restore_${restored.code ?? "error"}`);
this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
continue;
}
const currentState = this.wasmMotion.read().state;
const current = currentState.position;
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[pendingControl];
const joints = Array.isArray(currentState.joint_commanded) && currentState.joint_commanded.length === 5
? currentState.joint_commanded
: [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0];
const selected = typeof this.xyzacKinematics.selectPhysical === "function"
? this.xyzacKinematics.selectPhysical(this.xyzacProgramKinematicsMode, joints)
: this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, joints);
if (selected.status !== "verified") throw new Error(`wasm_switchkins_control_${this.pendingProgramControl}`);
if (this.planner === "linuxcnc-tp" && selected.pose) {
this.wasmMotion.setPosition(selected.pose);
if (typeof this.wasmMotion.setActiveKinematicsTag === "function") this.wasmMotion.setActiveKinematicsTag(this.xyzacProgramKinematicsMode);
}
this.pendingProgramControl = 0;
if (this.planner === "linuxcnc-tp" && pendingControl === 428) this.xyzacExecutionWaitNextMotion = true;
}
}
const steps = step.motion ? [step] : [];
program_steps.push(step);
for (const item of steps) {
item.motion.kinematics_mode = this.xyzacProgramKinematicsMode;
const linearNoop = item.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => item.motion.start[axis] === item.motion.end[axis]);
if (!linearNoop) {
const enqueue = this.wasmMotion.enqueue(item.motion);
if (enqueue.status === "error") throw new Error(enqueue.code);
const step = this.xyzacProgram.step();
if (step.status !== "verified") throw new Error(step.code);
// Path control is produced by the WASM interpreter and
// applied to the WASM trajectory queue before enqueue.
const motionControlMode = Number(step.program?.motion_control_mode ?? 0);
if (typeof this.wasmMotion.setTermCondition === "function") {
const mode = motionControlMode === 0 ? "continuous" : motionControlMode === 1 ? "exact_path" : "exact_stop";
this.wasmMotion.setTermCondition(mode, Number(step.program?.path_tolerance ?? 0));
} else {
const queueProfile = motionControlMode === 0 ? 0 : 1;
if (this.wasmMotion.controls.profile !== queueProfile) this.wasmMotion.setProfile(queueProfile);
}
program_steps.push(step);
if (step.has_control && [428, 429, 430].includes(step.control_code)) {
this.pendingProgramControl = step.control_code;
if (this.planner === "linuxcnc-tp") this.pendingTaskSync = 1;
}
if (step.motion) {
step.motion.kinematics_mode = this.xyzacProgramKinematicsMode;
step.motion.source_line = Number(step.program?.execution_source_line ?? step.line ?? step.motion.source_line ?? 0);
step.motion.source_file = step.program?.execution_source_file ?? this.xyzacProgram.path;
const linearNoop = step.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => step.motion.start[axis] === step.motion.end[axis]);
if (!linearNoop) {
// Commands issued by task become visible to motion on
// the following task cycle, matching LinuxCNC's NML
// handoff boundary and preventing a zero-time jump in
// current_line/exec_state snapshots.
const directDeferredMotion = this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && this.xyzacDeferredControlMotionType === 1 && Number(step.motion.kinematics_mode ?? 0) === XYZAC_MODES.identity;
this.pendingXyzacMotion = step.motion;
if (directDeferredMotion) {
this.xyzacAppendDeferredOnActive = true;
if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(2);
}
}
return;
}
if (step.program?.status === 4) return;
}
return;
}
@@ -1496,17 +1671,72 @@ export class SimSession {
this.task.setTick(nextTick);
const xyzacAuthority = Boolean(this.xyzacProgram);
const motionState = xyzacAuthority ? null : this.motion.step(nextTick);
const wasmMotion = this.wasmMotion.step();
if (wasmMotion.status === "error" && wasmMotion.code !== "wasm_motion_step_-605") throw new Error(wasmMotion.code);
// The selected LinuxCNC case has a 1 ms servo period and a
// 10 ms task period. A public SIM_STEP represents one task
// cycle, so advance the WASM motion queue by the configured
// servo/task ratio while publishing one stable task snapshot.
let wasmMotion = this.wasmMotion.read();
for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) {
if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") {
this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3);
}
const queueBeforeServo = Number(typeof this.wasmMotion.queueDepth === "function" ? this.wasmMotion.queueDepth() : this.wasmMotion.read().state.queue_depth ?? 0);
const beforeServoState = this.wasmMotion.read().state;
let servoResult;
servoResult = this.wasmMotion.step();
if (servoResult.status === "error" && servoResult.code !== "wasm_motion_step_-605") throw new Error(servoResult.code);
if (servoResult.status === "verified") {
wasmMotion = servoResult;
this.xyzacLastServoActiveId = Number(servoResult.state.active_id ?? 0);
const queuedEndpoint = this.xyzacLastQueuedMotion?.end;
const endpointNear = queuedEndpoint && Math.abs(Number(beforeServoState.position?.x ?? 0) - Number(queuedEndpoint.x ?? 0)) < 1e-3 && Math.abs(Number(beforeServoState.position?.y ?? 0) - Number(queuedEndpoint.y ?? 0)) < 1e-3 && Math.abs(Number(beforeServoState.position?.z ?? 0) - Number(queuedEndpoint.z ?? 0)) < 1e-3;
if (this.xyzacAppendDeferredOnActive && this.pendingXyzacMotion && Number(beforeServoState.active_id ?? 0) === this.xyzacDeferredCompletionActiveId && Number(beforeServoState.motion_type ?? 0) === 3 && (Number(beforeServoState.progress ?? 0) >= 0.999999 || (Number(beforeServoState.velocity ?? 0) <= 0.1 && endpointNear))) {
if (!this.xyzacDeferredPhysicalPoseApplied && this.xyzacDeferredPhysicalPose) {
this.wasmMotion.setPosition(this.xyzacDeferredPhysicalPose);
this.xyzacDeferredPhysicalPoseApplied = true;
}
const deferredMotion = this.pendingXyzacMotion;
const deferredEnqueue = this.wasmMotion.enqueue(deferredMotion);
if (deferredEnqueue.status === "error") throw new Error(deferredEnqueue.code);
this.xyzacLastEnqueuedKinematicsMode = Number(deferredMotion.kinematics_mode ?? 0);
this.xyzacLastQueuedMotion = deferredMotion;
this.xyzacAllowQueuedLookahead = true;
this.xyzacQueuedLookaheadTargetId = Number(deferredMotion.source_line ?? 0);
this.pendingXyzacMotion = null;
this.xyzacAppendDeferredOnActive = false;
if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(null);
servoResult.state.queue_depth = 2;
if (Array.isArray(this.wasmMotion.history) && this.wasmMotion.history.length) this.wasmMotion.history[this.wasmMotion.history.length - 1].queue_depth = 2;
}
if (this.planner === "linuxcnc-tp" && this.xyzacExecutionWaitSamples > 0 && Number(servoResult.state.active_id ?? 0) !== 0) this.xyzacExecutionWaitSamples -= 1;
if (this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && Number(servoResult.state.active_tag ?? -1) === this.xyzacDeferredPhysicalSwitchMode) {
const physicalJoints = Array.isArray(servoResult.state.joint_commanded) ? servoResult.state.joint_commanded : [];
const physical = this.xyzacKinematics.selectPhysical(this.xyzacDeferredPhysicalSwitchMode, physicalJoints);
if (physical.status !== "verified") throw new Error(`wasm_switchkins_deferred_${physical.code ?? "error"}`);
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
}
if (this.planner === "linuxcnc-tp" && sourceLookaheadReady && servo === 0 && this.pendingXyzacMotion) {
const lookaheadMode = Number(this.pendingXyzacMotion.kinematics_mode ?? 0);
const lookahead = this.wasmMotion.enqueue(this.pendingXyzacMotion);
if (lookahead.status === "error") throw new Error(lookahead.code);
this.xyzacLastEnqueuedKinematicsMode = lookaheadMode;
this.xyzacLastQueuedMotion = this.pendingXyzacMotion;
this.pendingXyzacMotion = null;
this.xyzacAllowQueuedLookahead = false;
this.xyzacQueuedLookaheadTargetId = 0;
sourceLookaheadReady = false;
wasmMotion = this.wasmMotion.read();
}
}
}
if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
if (this.motionAuthority === "wasm") {
if (wasmMotion.status === "verified") {
if (!xyzacAuthority) {
const position = this.motion.applyWasmPosition(wasmMotion.state.position, wasmMotion.state.velocity, nextTick);
if (position.status === "error") throw new Error(position.code);
} else if (this.xyzacKinematics && wasmMotion.state.active_id !== 0 && this.xyzacProgramKinematicsMode !== XYZAC_MODES.identity) {
const inverse = this.xyzacKinematics.inverse(wasmMotion.state.position);
if (inverse.status !== "verified") throw new Error(`wasm_xyzac_motion_inverse_${inverse.code ?? "error"}`);
}
}
if (xyzacAuthority) {
@@ -1561,12 +1791,12 @@ export class SimSession {
.filter((entry) => Number.isSafeInteger(entry.line) && entry.line > 0);
if (program) {
const status = this.xyzacProgram ? (program.status === 4 ? "completed" : program.status === 2 ? "running" : program.status === 3 ? "paused" : "loaded") : (program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status);
this.task.setProgram(status, this.xyzacProgram ? program.current_line : program.line);
this.task.setProgram(status, this.xyzacProgram ? (program.execution_source_line ?? program.current_line) : program.line);
if (status === "completed" && this.task.state.state === "RUNNING" && (this.motionAuthority !== "wasm" || this.wasmMotion.read().state.queue_depth === 0)) this.task.transition("STOPPED");
}
const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motionProjection(), plc: this.plc.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export() };
const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motionProjection(), plc: this.plc.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), xyzac_timing: { servo_cycle_time: this.xyzacMotionCycleTime, task_cycle_ticks: this.xyzacTaskCycleTicks } };
if (program) snapshotState.program = program;
const snapshot = this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
const snapshot = input.payload?.trace_only === true ? null : this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
const lightweightXyzac = Boolean(this.xyzacProgram);
return finish(this.envelope("COMMAND_RESULT", input, {
status: "verified", cycles, program_steps, execution_trace,
@@ -1574,14 +1804,31 @@ export class SimSession {
motion_authority: this.motionAuthority,
task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative,
wasm_motion: this.wasmMotion.read(), motion: snapshotState.motion,
...(lightweightXyzac ? {} : { plc: this.plc.export(), hal_runtime: snapshotState.hal_runtime, device_io: snapshotState.device_io, configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), snapshot }),
...(program ? { program } : {}), state_hash: snapshot.state_hash,
...(lightweightXyzac ? {} : { plc: this.plc.export(), hal_runtime: snapshotState.hal_runtime, device_io: snapshotState.device_io, configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), ...(snapshot ? { snapshot } : {}) }),
...(program ? { program } : {}), state_hash: snapshot?.state_hash ?? fnv1a64(snapshotState),
}));
}
case "PROGRAM_LOAD": {
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed");
const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source;
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;
const result = this.xyzacProgram.load(source, input.payload?.path ?? null);
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(input.payload?.path); if (subroutines.status !== "verified") return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); }
@@ -1614,7 +1861,7 @@ export class SimSession {
case "PROGRAM_RUN": {
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"); this.task.setProgram("running", this.xyzacProgram.snapshot().current_line); }
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");
}
const result = this.program.start();
@@ -1638,11 +1885,28 @@ 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;
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"); this.task.setProgram("paused", this.xyzacProgram.snapshot().current_line); }
else if (action === "resume") { call(this.core, "sim_protocol_resume"); this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); this.task.setProgram("running", this.xyzacProgram.snapshot().current_line); }
else { this.wasmMotion.abort(); this.task.transition("STOPPED"); this.task.setProgram("aborted", this.xyzacProgram.snapshot().current_line); }
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); }
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");
}
@@ -1673,6 +1937,23 @@ export class SimSession {
}
case "PROGRAM_RESTART": {
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;
const source = this.xyzacProgram.source;
const result = this.xyzacProgram.load(source, this.xyzacProgram.path);
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);