Run XYZAC through upstream LinuxCNC TP
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -2,7 +2,7 @@ import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { XyzacSession } from "./xyzac-session.mjs";
|
||||
|
||||
const core = await createModule();
|
||||
const session = new XyzacSession(core);
|
||||
const session = new XyzacSession(core, { planner: "linuxcnc-tp" });
|
||||
|
||||
self.onmessage = (event) => {
|
||||
try {
|
||||
|
||||
254
web/worker/wasm-linuxcnc-tp.mjs
Normal file
254
web/worker/wasm-linuxcnc-tp.mjs
Normal file
@@ -0,0 +1,254 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
|
||||
const POSE_BYTES = 72;
|
||||
const STATE_BYTES = 112;
|
||||
const SOURCE_TP_SCHEMA = "linuxcnc_source_tp_state.v1";
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const fn = core?.[`_${name}`] ?? core?.[name];
|
||||
if (typeof fn === "function") return fn(...args);
|
||||
if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args);
|
||||
throw new Error(`missing core export: ${name}`);
|
||||
}
|
||||
|
||||
function poseArray(value = {}) { return ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => Number(value[axis] ?? 0)); }
|
||||
function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis, index) => [axis, Number(values[index] ?? 0)])); }
|
||||
|
||||
function cubicState() { return { x0: 0, x1: 0, x2: 0, x3: 0, t: 0, need: true, coeff: [0, 0, 0, 0] }; }
|
||||
function cubicAdd(state, point, segmentTime = 0.001) {
|
||||
if (!state.need) return;
|
||||
state.x0 = state.x1; state.x1 = state.x2; state.x2 = state.x3; state.x3 = Number(point);
|
||||
const wp0 = (state.x0 + 4 * state.x1 + state.x2) / 6;
|
||||
const wp1 = (state.x1 + 4 * state.x2 + state.x3) / 6;
|
||||
const vel0 = (state.x2 - state.x0) / (2 * segmentTime);
|
||||
const vel1 = (state.x3 - state.x1) / (2 * segmentTime);
|
||||
const b = 3 * (wp1 - wp0) / (segmentTime * segmentTime) - (2 * vel0 + vel1) / segmentTime;
|
||||
const a = (vel1 - vel0) / (3 * segmentTime * segmentTime) - 2 * b / (3 * segmentTime);
|
||||
state.coeff = [a, b, vel0, wp0]; state.t = 0; state.need = false;
|
||||
}
|
||||
function cubicInterpolate(state, segmentTime = 0.001) {
|
||||
if (state.need) cubicAdd(state, state.x3, segmentTime);
|
||||
const [a, b, c, d] = state.coeff;
|
||||
const t = state.t;
|
||||
const value = ((a * t + b) * t + c) * t + d;
|
||||
state.t += segmentTime;
|
||||
if (Math.abs(segmentTime - state.t) < segmentTime * 0.5) state.need = true;
|
||||
return value;
|
||||
}
|
||||
|
||||
export class WasmLinuxcncTp {
|
||||
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300 } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.cycleTime = cycleTime;
|
||||
this.velocityLimit = velocityLimit;
|
||||
this.accelerationLimit = accelerationLimit;
|
||||
this.queueCapacity = 2000;
|
||||
this.planner = 0;
|
||||
this.posePointer = 0;
|
||||
this.centerPointer = 0;
|
||||
this.normalPointer = 0;
|
||||
this.statePointer = 0;
|
||||
this.history = [];
|
||||
this.activeTag = 0;
|
||||
this.segmentTags = new Map();
|
||||
this.lastActiveId = 0;
|
||||
// Task execution state is owned by the LinuxCNC task/TP handoff, not by
|
||||
// the TP C struct. Keep it alongside the source-TP state so every servo
|
||||
// sample carries the same WAITING_FOR_MOTION boundary as native LinuxCNC.
|
||||
this.executionState = 3;
|
||||
this.lastState = null;
|
||||
this.queueDepthOverride = null;
|
||||
this.kinematics = null;
|
||||
this.cubic = Array.from({ length: 5 }, () => cubicState());
|
||||
this.previousJointCommanded = [0, 0, 0, 0, 0];
|
||||
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, paused: false };
|
||||
if (!this.available || typeof core?._malloc !== "function") return;
|
||||
this.planner = core._malloc(call(core, "sim_linuxcnc_tp_size", "number", [], []));
|
||||
this.posePointer = core._malloc(POSE_BYTES);
|
||||
this.centerPointer = core._malloc(24);
|
||||
this.normalPointer = core._malloc(24);
|
||||
this.statePointer = core._malloc(STATE_BYTES);
|
||||
if (![this.planner, this.posePointer, this.centerPointer, this.normalPointer, this.statePointer].every(Boolean)) throw new Error("linuxcnc_tp_allocation_failed");
|
||||
const result = call(core, "sim_linuxcnc_tp_init", "number", ["number", "number", "number", "number"], [this.planner, cycleTime, velocityLimit, accelerationLimit]);
|
||||
if (result !== 0) throw new Error(`linuxcnc_tp_init_${result}`);
|
||||
this.setPosition({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
|
||||
this.setProfile(0);
|
||||
}
|
||||
|
||||
setPosition(value) {
|
||||
if (!this.available) return this.read();
|
||||
this.core.HEAPF64.set(poseArray(value), this.posePointer / 8);
|
||||
const result = call(this.core, "sim_linuxcnc_tp_set_position", "number", ["number", "number"], [this.planner, this.posePointer]);
|
||||
if (result !== 0) throw new Error(`linuxcnc_tp_set_position_${result}`);
|
||||
return this.read();
|
||||
}
|
||||
|
||||
setGoalPosition(value) {
|
||||
if (!this.available) return this.read();
|
||||
this.core.HEAPF64.set(poseArray(value), this.posePointer / 8);
|
||||
const result = call(this.core, "sim_linuxcnc_tp_set_goal_position", "number", ["number", "number"], [this.planner, this.posePointer]);
|
||||
if (result !== 0) throw new Error(`linuxcnc_tp_set_goal_position_${result}`);
|
||||
return this.read();
|
||||
}
|
||||
|
||||
setProfile(profile, tolerance = 0) {
|
||||
const condition = profile === 0 ? 2 : profile === 1 ? 0 : profile === 2 ? 1 : -1;
|
||||
if (![0, 1, 2].includes(profile) || !Number.isFinite(Number(tolerance)) || Number(tolerance) < 0) throw new Error("linuxcnc_tp_profile_invalid");
|
||||
if (this.available) {
|
||||
const result = call(this.core, "sim_linuxcnc_tp_set_profile", "number", ["number", "number", "number"], [this.planner, condition, Number(tolerance)]);
|
||||
if (result !== 0) throw new Error(`linuxcnc_tp_profile_${result}`);
|
||||
}
|
||||
this.controls.profile = profile;
|
||||
return this.read();
|
||||
}
|
||||
|
||||
setTermCondition(mode, tolerance = 0) {
|
||||
const profile = mode === "continuous" || mode === 0 ? 0 : mode === "exact_path" || mode === 1 ? 2 : 1;
|
||||
return this.setProfile(profile, tolerance);
|
||||
}
|
||||
|
||||
setLookahead(value) { if (!Number.isSafeInteger(value) || value < 1) throw new Error("linuxcnc_tp_lookahead_invalid"); this.controls.lookahead = value; return this.read(); }
|
||||
setDynamics(accelerationLimit, jerkLimit) { this.accelerationLimit = Number(accelerationLimit); this.controls.acceleration_limit = this.accelerationLimit; this.controls.jerk_limit = Number(jerkLimit); if (this.available) call(this.core, "sim_linuxcnc_tp_set_dynamics", "number", ["number", "number"], [this.planner, this.accelerationLimit]); return this.read(); }
|
||||
setFeedOverride(value) { this.controls.feed_override = Number(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_feed_override", "number", ["number", "number"], [this.planner, this.controls.feed_override]); return this.read(); }
|
||||
setPaused(value) { this.controls.paused = Boolean(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_paused", "number", ["number", "number"], [this.planner, this.controls.paused ? 1 : 0]); return this.read(); }
|
||||
|
||||
enqueue(segment) {
|
||||
if (!this.available || !segment) return { status: "skipped", code: "linuxcnc_tp_unavailable" };
|
||||
this.core.HEAPF64.set(poseArray(segment.end), this.posePointer / 8);
|
||||
const canonMotionType = Number(segment.canon_motion_type ?? (segment.motion_type === 2 ? 3 : segment.kind === "rapid" ? 1 : 2));
|
||||
this.activeTag = Number(segment.kinematics_mode ?? segment.tag ?? 0);
|
||||
const segmentTag = this.activeTag;
|
||||
const segmentId = Number(segment.source_line ?? 0);
|
||||
if (segmentId > 0) this.segmentTags.set(segmentId, segmentTag);
|
||||
const acceleration = Math.max(Number(segment.acceleration ?? 0), this.accelerationLimit);
|
||||
const result = segment.motion_type === 2
|
||||
? (this.core.HEAPF64.set([Number(segment.center?.x ?? 0), Number(segment.center?.y ?? 0), Number(segment.center?.z ?? 0)], this.centerPointer / 8), this.core.HEAPF64.set([Number(segment.normal?.x ?? 0), Number(segment.normal?.y ?? 0), Number(segment.normal?.z ?? 0)], this.normalPointer / 8), call(this.core, "sim_linuxcnc_tp_add_circle", "number", ["number", "number", "number", "number", "number", "number", "number", "number", "number"], [this.planner, this.posePointer, this.centerPointer, this.normalPointer, Number(segment.turn ?? 1), Number(segment.velocity ?? 0), acceleration, Number(segment.source_line ?? 0), canonMotionType]))
|
||||
: call(this.core, "sim_linuxcnc_tp_add_line", "number", ["number", "number", "number", "number", "number", "number"], [this.planner, this.posePointer, Number(segment.velocity ?? 0), acceleration, Number(segment.source_line ?? 0), canonMotionType]);
|
||||
if (result !== 0) return { status: "error", code: `linuxcnc_tp_enqueue_${result}` };
|
||||
if (this.lastState?.active_id && Number(this.lastState.active_id) !== segmentId) this.activeTag = Number(this.lastState.active_tag ?? this.activeTag);
|
||||
return { status: "verified", source_line: segment.source_line ?? null };
|
||||
}
|
||||
|
||||
setKinematics(kinematics) { this.kinematics = kinematics ?? null; return this.read(); }
|
||||
|
||||
setActiveKinematicsTag(mode) {
|
||||
this.activeTag = Number(mode) === 0 ? 0 : Number(mode);
|
||||
return this.read();
|
||||
}
|
||||
|
||||
setExecutionState(value) {
|
||||
const state = Number(value);
|
||||
if (![2, 3, 4].includes(state)) throw new Error("linuxcnc_tp_execution_state_invalid");
|
||||
this.executionState = state;
|
||||
// The next source TP step publishes the state. Avoid a redundant C read
|
||||
// on every servo tick merely to attach this task/TP handoff marker.
|
||||
return { status: "verified", state: this.lastState };
|
||||
}
|
||||
|
||||
queueDepth() { return Number(this.lastState?.queue_depth ?? 0); }
|
||||
|
||||
setQueueDepthOverride(value) {
|
||||
this.queueDepthOverride = value === null ? null : Math.max(0, Number(value));
|
||||
if (this.lastState && this.queueDepthOverride !== null) this.lastState.queue_depth = this.queueDepthOverride;
|
||||
return this.read();
|
||||
}
|
||||
|
||||
commandedJoints(position, activeTag) {
|
||||
if (Number(activeTag) === 0) return [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
|
||||
const inverse = this.kinematics?.inverseMode?.(Number(activeTag), position) ?? this.kinematics?.inverse?.(position);
|
||||
if (inverse?.status === "verified" && Array.isArray(inverse.joints)) return inverse.joints.slice(0, 5);
|
||||
return this.previousJointCommanded.slice();
|
||||
}
|
||||
|
||||
step() {
|
||||
if (!this.available) return { status: "blocked", code: "linuxcnc_tp_unavailable" };
|
||||
if (this.controls.paused) return { status: "error", code: "linuxcnc_tp_paused" };
|
||||
const result = call(this.core, "sim_linuxcnc_tp_step", "number", ["number"], [this.planner]);
|
||||
if (result !== 0) return { status: "error", code: `linuxcnc_tp_step_${result}` };
|
||||
const state = this.read();
|
||||
if (state.status === "verified") {
|
||||
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
|
||||
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
|
||||
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
|
||||
state.state.joint_commanded = commanded;
|
||||
state.state.joint_actual = this.previousJointCommanded.slice();
|
||||
this.previousJointCommanded = commanded.slice();
|
||||
}
|
||||
if (state.status === "verified") this.history.push(structuredClone(state.state));
|
||||
return state;
|
||||
}
|
||||
|
||||
idle() {
|
||||
if (!this.available) return this.read();
|
||||
const result = call(this.core, "sim_linuxcnc_tp_idle", "number", ["number"], [this.planner]);
|
||||
if (result !== 0) return { status: "error", code: `linuxcnc_tp_idle_${result}` };
|
||||
const state = this.read();
|
||||
if (state.status === "verified") {
|
||||
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
|
||||
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
|
||||
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
|
||||
state.state.joint_commanded = commanded;
|
||||
state.state.joint_actual = this.previousJointCommanded.slice();
|
||||
this.previousJointCommanded = commanded.slice();
|
||||
}
|
||||
if (state.status === "verified") this.history.push(structuredClone(state.state));
|
||||
return state;
|
||||
}
|
||||
|
||||
drainTrace() { const trace = this.history; this.history = []; return trace; }
|
||||
|
||||
read() {
|
||||
if (!this.available) return { status: "blocked", state: { position: toPose([]), velocity: 0, progress: 0, tick: 0, queue_depth: 0, active_id: 0, active_tag: 0, motion_type: 0, status: -1214, exec_state: this.executionState, source: "linuxcnc-source-tp" } };
|
||||
const result = call(this.core, "sim_linuxcnc_tp_read_state", "number", ["number", "number"], [this.planner, this.statePointer]);
|
||||
if (result !== 0) return { status: "error", code: `linuxcnc_tp_read_${result}` };
|
||||
const doubles = this.core.HEAPF64.subarray(this.statePointer / 8, this.statePointer / 8 + 11);
|
||||
const ints = this.core.HEAPU32.subarray(this.statePointer / 4 + 22, this.statePointer / 4 + 25);
|
||||
const activeId = ints[2];
|
||||
if (activeId && activeId !== this.lastActiveId && this.segmentTags.has(activeId)) this.activeTag = this.segmentTags.get(activeId);
|
||||
this.lastActiveId = activeId;
|
||||
const state = { position: toPose(doubles), velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: this.previousJointCommanded.slice(), joint_actual: this.previousJointCommanded.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, source: "linuxcnc-source-tp" };
|
||||
this.lastState = state;
|
||||
return { status: "verified", state };
|
||||
}
|
||||
|
||||
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; this.cubic = Array.from({ length: 5 }, () => cubicState()); this.previousJointCommanded = [0, 0, 0, 0, 0]; return this.read(); }
|
||||
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
|
||||
exportState() {
|
||||
const current = this.read();
|
||||
const segments = [];
|
||||
if (this.available) {
|
||||
const depth = Number(call(this.core, "sim_linuxcnc_tp_queue_depth", "number", ["number"], [this.planner]));
|
||||
const size = 240;
|
||||
const pointer = this.core._malloc(size);
|
||||
try {
|
||||
for (let index = 0; index < depth; index += 1) {
|
||||
if (call(this.core, "sim_linuxcnc_tp_queue_segment", "number", ["number", "number", "number"], [this.planner, index, pointer]) !== 0) continue;
|
||||
const values = this.core.HEAPF64.subarray(pointer / 8, pointer / 8 + 29);
|
||||
const ints = this.core.HEAP32.subarray(pointer / 4 + 52, pointer / 4 + 54);
|
||||
const segmentView = new DataView(this.core.HEAPU8.buffer, pointer, size);
|
||||
const id = this.core.HEAPU32[pointer / 4 + 54];
|
||||
segments.push({ start: toPose(values), end: toPose(values.subarray(9)), center: { x: values[18], y: values[19], z: values[20] }, normal: { x: values[21], y: values[22], z: values[23] }, velocity: values[24], acceleration: values[25], motion_type: ints[0], turn: ints[1], id, kinematics_mode: this.segmentTags.get(id) ?? this.activeTag, term_cond: this.core.HEAP32[pointer / 4 + 55], progress: segmentView.getFloat64(224, true), target: segmentView.getFloat64(232, true) });
|
||||
}
|
||||
} finally { this.core._free(pointer); }
|
||||
}
|
||||
const unsigned = { schema: SOURCE_TP_SCHEMA, cycle_time: this.cycleTime, velocity_limit: this.velocityLimit, controls: { ...this.controls }, execution_state: this.executionState, active_tag: this.activeTag, state: current.state, segments };
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
restoreState(value) {
|
||||
if (!value || value.schema !== SOURCE_TP_SCHEMA || value.cycle_time !== this.cycleTime || value.velocity_limit !== this.velocityLimit || value.state_hash !== fnv1a64(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "state_hash")))) throw new Error("linuxcnc_tp_restore_schema");
|
||||
this.reset();
|
||||
this.executionState = Number(value.execution_state ?? 3);
|
||||
this.setProfile(value.controls?.profile ?? 0, value.controls?.tolerance ?? 0);
|
||||
this.setFeedOverride(value.controls?.feed_override ?? 1);
|
||||
this.setPosition(value.state?.position ?? {});
|
||||
for (const segment of value.segments ?? []) this.enqueue({ ...segment, kinematics_mode: segment.kinematics_mode ?? value.active_tag ?? 0 });
|
||||
if (this.available && value.state) {
|
||||
this.core.HEAPF64.set(poseArray(value.state.position), this.posePointer / 8);
|
||||
const restored = call(this.core, "sim_linuxcnc_tp_restore_progress", "number", ["number", "number", "number", "number"], [this.planner, this.posePointer, Number(value.state.velocity ?? 0), Number(value.state.tick ?? 0)]);
|
||||
if (restored !== 0) throw new Error(`linuxcnc_tp_restore_progress_${restored}`);
|
||||
}
|
||||
return this.read();
|
||||
}
|
||||
destroy() { for (const pointer of [this.planner, this.posePointer, this.centerPointer, this.normalPointer, this.statePointer]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer); this.planner = this.posePointer = this.centerPointer = this.normalPointer = this.statePointer = 0; }
|
||||
}
|
||||
@@ -76,6 +76,7 @@ export class WasmProgramRuntime {
|
||||
this.subroutines = [];
|
||||
this.executionSource = "";
|
||||
this.executionLines = [];
|
||||
this.executionLineMetadata = [];
|
||||
this.rapidVelocity = 35;
|
||||
if (!this.available || typeof core?._malloc !== "function") return;
|
||||
this.pointer = core._malloc(call(core, "sim_program_size", "number", [], []));
|
||||
@@ -108,6 +109,11 @@ export class WasmProgramRuntime {
|
||||
this.subroutines = [];
|
||||
this.executionSource = source;
|
||||
this.executionLines = source.split(/\r?\n/);
|
||||
this.executionLineMetadata = this.executionLines.map((_, index) => ({ path: this.path, line: index + 1 }));
|
||||
while (this.executionLines.length && this.executionLines.at(-1) === "") {
|
||||
this.executionLines.pop();
|
||||
this.executionLineMetadata.pop();
|
||||
}
|
||||
return { status: "verified", program: this.snapshot() };
|
||||
} finally { this.core._free(pointer); }
|
||||
}
|
||||
@@ -133,7 +139,7 @@ export class WasmProgramRuntime {
|
||||
if (loaded.status !== "verified") return loaded;
|
||||
temporary.setRapidVelocity(this.rapidVelocity);
|
||||
for (const subroutine of this.subroutines) {
|
||||
const added = temporary.addSubroutine(subroutine.name, subroutine.source);
|
||||
const added = temporary.addSubroutine(subroutine.name, subroutine.source, subroutine.path);
|
||||
if (added.status !== "verified") return added;
|
||||
}
|
||||
const started = temporary.start();
|
||||
@@ -159,7 +165,7 @@ export class WasmProgramRuntime {
|
||||
if (result !== 0) throw new Error(`wasm_program_set_rapid_velocity_${result}`);
|
||||
return { status: "verified", command: "sim_program_set_rapid_velocity" };
|
||||
}
|
||||
addSubroutine(name, source) {
|
||||
addSubroutine(name, source, path = null) {
|
||||
if (typeof source !== "string" || source.length === 0) return { status: "error", code: "program_subroutine_invalid" };
|
||||
const bytes = new TextEncoder().encode(source);
|
||||
const pointer = this.core._malloc(bytes.byteLength);
|
||||
@@ -167,12 +173,19 @@ export class WasmProgramRuntime {
|
||||
try {
|
||||
const code = call(this.core, "sim_program_add_subroutine", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]);
|
||||
if (code !== 0) return { status: "error", code: `wasm_program_add_subroutine_${code}`, result: code };
|
||||
this.subroutines.push({ name: String(name ?? "subroutine"), source });
|
||||
if (this.executionSource && !this.executionSource.endsWith("\n")) this.executionSource += "\n";
|
||||
if (this.executionLines.length && this.executionLines.at(-1) !== "") this.executionLines.push("");
|
||||
if (this.executionLines.length && this.executionLines.at(-1) !== "") {
|
||||
this.executionLines.push("");
|
||||
this.executionLineMetadata.push({ path: null, line: 0 });
|
||||
}
|
||||
this.executionSource += source;
|
||||
if (this.executionLines.length && this.executionLines.at(-1) === "") this.executionLines.pop();
|
||||
if (this.executionLines.length && this.executionLines.at(-1) === "") {
|
||||
this.executionLines.pop();
|
||||
this.executionLineMetadata.pop();
|
||||
}
|
||||
this.executionLines.push(...source.split(/\r?\n/));
|
||||
this.executionLineMetadata.push(...source.split(/\r?\n/).map((_, index) => ({ path, line: index + 1 })));
|
||||
this.subroutines.push({ name: String(name ?? "subroutine"), path, source });
|
||||
return { status: "verified", command: "sim_program_add_subroutine", name: String(name ?? "subroutine") };
|
||||
} finally { this.core._free(pointer); }
|
||||
}
|
||||
@@ -268,7 +281,8 @@ export class WasmProgramRuntime {
|
||||
const programStatus = { 0: "idle", 1: "loaded", 2: "running", 3: "paused", 4: "completed", 5: "aborted", 6: "fault" }[state.status] ?? "idle";
|
||||
const motionControl = { 0: "continuous", 1: "exact_path", 2: "exact_stop" }[state.motion_control_mode] ?? "continuous";
|
||||
const executionLineText = state.current_line > 0 ? (this.executionLines[state.current_line - 1] ?? "").trim() : "";
|
||||
return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, motion_control: { mode: motionControl, path_tolerance: Number(state.path_tolerance ?? 0), naive_cam_tolerance: Number(state.naive_cam_tolerance ?? 0) }, active_gcodes: activeGcodes(state), loaded: state.status !== 0, line: state.current_line, currentLine: state.current_line, execution_line: state.current_line, execution_line_text: executionLineText, program_status: programStatus, path: this.path, subroutines: this.subroutines.map((subroutine) => subroutine.name), state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, source_hash: this.source ? fnv1a64(this.source) : null };
|
||||
const executionMeta = state.current_line > 0 ? (this.executionLineMetadata[state.current_line - 1] ?? { path: this.path, line: state.current_line }) : { path: this.path, line: 0 };
|
||||
return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, motion_control: { mode: motionControl, path_tolerance: Number(state.path_tolerance ?? 0), naive_cam_tolerance: Number(state.naive_cam_tolerance ?? 0) }, active_gcodes: activeGcodes(state), loaded: state.status !== 0, line: state.current_line, currentLine: state.current_line, execution_line: state.current_line, execution_line_text: executionLineText, execution_source_file: executionMeta.path, execution_source_line: executionMeta.line, program_status: programStatus, path: this.path, subroutines: this.subroutines.map((subroutine) => subroutine.name), state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, source_hash: this.source ? fnv1a64(this.source) : null };
|
||||
} finally { this.core._free(buffer); }
|
||||
}
|
||||
|
||||
@@ -278,5 +292,6 @@ export class WasmProgramRuntime {
|
||||
this.pointer = this.motionPointer = this.samplePointer = this.samplePosePointer = 0;
|
||||
this.executionSource = "";
|
||||
this.executionLines = [];
|
||||
this.executionLineMetadata = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export class WasmXyzacKinematics {
|
||||
constructor(core, options = {}) {
|
||||
this.core = core;
|
||||
this.options = { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options };
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_switch_init", "sim_kinematics_switch_register", "sim_kinematics_switch_select", "sim_kinematics_switch_forward", "sim_kinematics_switch_inverse", "sim_kinematics_switch_active", "sim_kinematics_switch_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_switch_init", "sim_kinematics_switch_register", "sim_kinematics_switch_select", "sim_kinematics_switch_select_physical", "sim_kinematics_switch_forward", "sim_kinematics_switch_inverse", "sim_kinematics_switch_inverse_mode", "sim_kinematics_switch_active", "sim_kinematics_switch_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.lastCommand = null;
|
||||
this.modules = new Map();
|
||||
this.pointers = [];
|
||||
@@ -128,6 +128,14 @@ export class WasmXyzacKinematics {
|
||||
return this.snapshot();
|
||||
}
|
||||
|
||||
inverseMode(mode, currentPose) {
|
||||
if (!this.available) return { status: "blocked", code: "wasm_xyzac_unavailable" };
|
||||
this.setPose(currentPose);
|
||||
const result = call(this.core, "sim_kinematics_switch_inverse_mode", "number", ["number", "number", "number", "number", "number", "number"], [this.switchPointer, mode, this.posePointer, this.jointPointer, 5, this.resultPointer]);
|
||||
if (result !== 0) return resultCode("inverse_mode", result);
|
||||
return { status: "verified", joints: [...this.core.HEAPF64.subarray(this.jointPointer / 8, this.jointPointer / 8 + 5)] };
|
||||
}
|
||||
|
||||
select(mode, joints = [0, 0, 0, 0, 0]) {
|
||||
if (![MODE.identity, MODE.xyzac, MODE.userk].includes(mode)) return { status: "error", code: "wasm_xyzac_invalid_mode" };
|
||||
this.setJoints(joints);
|
||||
@@ -137,6 +145,16 @@ export class WasmXyzacKinematics {
|
||||
return this.snapshot();
|
||||
}
|
||||
|
||||
selectPhysical(mode, joints = [0, 0, 0, 0, 0]) {
|
||||
if (!this.available) return { status: "blocked", code: "wasm_xyzac_unavailable" };
|
||||
if (![MODE.identity, MODE.xyzac, MODE.userk].includes(mode)) return { status: "error", code: "wasm_xyzac_invalid_mode" };
|
||||
this.setJoints(joints);
|
||||
const result = call(this.core, "sim_kinematics_switch_select_physical", "number", ["number", "number", "number", "number", "number", "number"], [this.switchPointer, mode, this.jointPointer, 5, this.posePointer, this.resultPointer]);
|
||||
if (result !== 0) return resultCode("switch_physical", result);
|
||||
this.lastCommand = `M${mode === MODE.xyzac ? 428 : mode === MODE.identity ? 429 : 430}`;
|
||||
return this.snapshot();
|
||||
}
|
||||
|
||||
restoreState(state) {
|
||||
if (!state || state.schema !== "wasm-xyzac-kinematics.v1" || ![MODE.identity, MODE.xyzac, MODE.userk].includes(state.active_type) || !Array.isArray(state.joints) || state.joints.length !== 5) throw new Error("wasm_xyzac_restore_state_invalid");
|
||||
this.initializeSwitch();
|
||||
|
||||
Reference in New Issue
Block a user