Complete XYZAC LinuxCNC WASM parity and smooth 3D preview
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
|
||||
import { capabilities, validateLimit } from "./capabilities.mjs";
|
||||
import { mapCoreError } from "./error-map.mjs";
|
||||
import { createCheckpoint, recoverSimulation } from "../workspace/recovery.mjs";
|
||||
import { createCheckpoint, recoverSimulation, validateCheckpoint } from "../workspace/recovery.mjs";
|
||||
import { preflightConfiguration } from "../workspace/configuration.mjs";
|
||||
import { ConfigurationStartupStateMachine } from "../workspace/configuration.mjs";
|
||||
import { preflightHalConfiguration } from "../workspace/hal-configuration.mjs";
|
||||
@@ -23,7 +23,11 @@ import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
|
||||
import { axisState } from "./axis-state.mjs";
|
||||
import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
|
||||
import { WasmHomeLimit } from "./wasm-home-limit.mjs";
|
||||
import { axisGate } from "../app/axis-contract.mjs";
|
||||
import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs";
|
||||
import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
|
||||
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
|
||||
import { WasmProgramRuntime } from "./wasm-program-runtime.mjs";
|
||||
import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
|
||||
|
||||
const PROTOCOL_SCHEMA = 1;
|
||||
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
||||
@@ -41,6 +45,7 @@ const OPERATION_KINDS = new Set([
|
||||
"PYTHON_IMPORT", "PYTHON_CALL", "PYTHON_CAPABILITY",
|
||||
"PROGRAM_STEP", "PROGRAM_SEEK", "MDI_EXECUTE", "PLC_CONFIG", "PLC_SET", "PLC_TRIGGER", "FOLLOWING_ERROR", "IO_COMMAND", "IO_ACK", "IO_TIMEOUT",
|
||||
"REMAP_REGISTER", "REMAP_CALL", "VCP_REGISTER", "VCP_UPDATE", "FILTER_EXEC",
|
||||
"POWER", "KINEMATICS_SELECT", "VISUAL_CLEAR",
|
||||
]);
|
||||
|
||||
const PANEL_GATE_ACTIONS = Object.freeze({
|
||||
@@ -48,6 +53,7 @@ const PANEL_GATE_ACTIONS = Object.freeze({
|
||||
PROGRAM_RESUME: "resume", PROGRAM_ABORT: "abort", PROGRAM_RESTART: "restart",
|
||||
JOG_START: "jogStart", JOG_STOP: "jogStop", HOME: "home", SET_OVERRIDE: "override",
|
||||
ESTOP: "estop", IO_RECOVER: "recover",
|
||||
POWER: "power",
|
||||
});
|
||||
|
||||
function text(value) {
|
||||
@@ -102,21 +108,33 @@ export class SimSession {
|
||||
this.authoritative = new AuthoritativeRuntime();
|
||||
this.wasmMotion = new WasmMotionQueue(core);
|
||||
this.wasmHomeLimit = new WasmHomeLimit(core);
|
||||
this.xyzacKinematics = null;
|
||||
this.xyzacIo = null;
|
||||
this.xyzacDeviceIo = null;
|
||||
this.xyzacProgram = null;
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
this.xyzacRapidVelocity = 35;
|
||||
this.pendingProgramControl = 0;
|
||||
this.motionAuthority = "js";
|
||||
this.task = new SimTask(core, { contextId, sessionId: sessionId || null });
|
||||
this.scheduler = new SimScheduler(core);
|
||||
this.configurationState = null;
|
||||
this.halState = null;
|
||||
this.previewState = null;
|
||||
this.workspaceFiles = {};
|
||||
call(core, "sim_protocol_reset");
|
||||
if (typeof core.ccall === "function") core.ccall("sim_event_reset", null, [], []);
|
||||
}
|
||||
|
||||
envelope(kind, input, payload, tick = this.tick) {
|
||||
const statePayload = payload && typeof payload === "object" ? payload : {};
|
||||
const program = statePayload.program ?? (this.program.loaded ? this.program.snapshot() : {});
|
||||
const program = statePayload.program ?? (this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {});
|
||||
const task = statePayload.task ?? this.task.export();
|
||||
const panelSnapshot = this.panelSnapshot({ task, program, input, tick });
|
||||
// The loaded toolpath is immutable during execution. Avoid cloning all
|
||||
// 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 panelSnapshot = this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep });
|
||||
return {
|
||||
schema_version: PROTOCOL_SCHEMA,
|
||||
kind,
|
||||
@@ -130,39 +148,114 @@ export class SimSession {
|
||||
panel_snapshot: panelSnapshot,
|
||||
motion_authority: statePayload.motion_authority ?? this.motionAuthority,
|
||||
task_authority: statePayload.task_authority ?? this.task.authority(),
|
||||
wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read(),
|
||||
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}),
|
||||
axis_state: axisState({ task, program, action: input.kind, payload: input.payload ?? {} }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
panelSnapshot({ task = this.task.export(), program = this.program.loaded ? this.program.snapshot() : null, input = {}, tick = this.tick } = {}) {
|
||||
const motion = this.motion.export();
|
||||
wasmMotionProjection() {
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
const position = wasmMotion.status === "verified" ? wasmMotion.state.position : { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 };
|
||||
const home = this.wasmHomeLimit.read();
|
||||
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 unsigned = {
|
||||
schema: "sim_motion.v1",
|
||||
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]),
|
||||
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
|
||||
feed_override: 1,
|
||||
spindle_override: 1,
|
||||
homed: Boolean(home.state?.homed),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(home.state?.homed && index < (home.state?.joint_count ?? 0))),
|
||||
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
|
||||
on_any_limit: Boolean(home.state?.limit_error),
|
||||
following_error: 0,
|
||||
jog: null,
|
||||
estop: Boolean(ioState?.estop),
|
||||
fault: ioState?.fault ? (ioState.fault_reason ?? "io_fault") : null,
|
||||
};
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
|
||||
motionProjection() {
|
||||
if (this.xyzacProgram) return this.wasmMotionProjection();
|
||||
if (this.motionAuthority !== "wasm") return this.motion.export();
|
||||
const wasm = this.wasmMotion.read();
|
||||
return wasm.status === "verified" ? { ...this.motion.export(), axes: { ...wasm.state.position } } : this.motion.export();
|
||||
}
|
||||
|
||||
wasmDeviceProjection({ includeBytes = false } = {}) {
|
||||
if (!this.xyzacIo) return this.devices.export();
|
||||
const state = this.xyzacIo.canonical();
|
||||
if (!state || state.status === "blocked") return { schema: "wasm_xyzac_devices.v1", source: "wasm_io", status: "blocked" };
|
||||
const ioExport = includeBytes ? this.xyzacIo.export() : null;
|
||||
const unsigned = { schema: "wasm_xyzac_devices.v1", source: "wasm_io", ...state, ...(includeBytes ? { state_bytes: ioExport.state_bytes } : {}) };
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
|
||||
loadXyzacSubroutines(programPath) {
|
||||
if (!this.xyzacProgram) return { status: "verified", count: 0 };
|
||||
const requirement = this.xyzacProgram.requiresSubroutines();
|
||||
if (requirement.status !== "verified") return requirement;
|
||||
if (!requirement.required) return { status: "verified", count: 0 };
|
||||
const basePath = String(programPath ?? "").split("/").slice(0, -1).join("/");
|
||||
const rows = [["remap_subs/xyzac_switchkins_sub.ngc", "xyzac_switchkins_sub"], ["remap_subs/helix_ac.ngc", "helix_ac"]];
|
||||
for (const [suffix, name] of rows) {
|
||||
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);
|
||||
if (added.status !== "verified") return added;
|
||||
}
|
||||
return { status: "verified", count: rows.length };
|
||||
}
|
||||
|
||||
panelSnapshot({ task = this.task.export(), program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : null, input = {}, tick = this.tick, motionTrace = null, includeToolpath = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeConfiguration = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeStableRuntime = input.kind !== "SIM_STEP" || !this.xyzacProgram } = {}) {
|
||||
const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
const homeLimit = this.wasmHomeLimit.read();
|
||||
const devices = this.devices.export();
|
||||
const deviceIo = this.deviceIo.export();
|
||||
const halRuntime = this.hal.export();
|
||||
const authoritative = this.authoritative.export();
|
||||
const devices = this.wasmDeviceProjection();
|
||||
const deviceIo = this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export();
|
||||
const halRuntime = this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export();
|
||||
const authoritative = this.xyzacProgram
|
||||
? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) }
|
||||
: this.authoritative.export();
|
||||
const axis = axisState({ task, program: program ?? {}, action: input.kind, payload: input.payload ?? {} });
|
||||
const gateInput = { task, program: program ?? {}, motion, devices, taskfile: Boolean(this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
|
||||
const gateActions = ["load", "run", "step", "pause", "resume", "abort", "restart", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "reset", "spindle", "coolant"];
|
||||
const gates = Object.fromEntries(gateActions.map((action) => [action, axisGate(action, gateInput)]));
|
||||
const position = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
|
||||
const xyzac = this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
|
||||
const gateInput = { task, program: program ?? {}, motion, devices, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
|
||||
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
|
||||
const gates = { ...Object.fromEntries(gateActions.map((action) => [action, axisGate(action, gateInput)])), ...xyzacGateMatrix(gateInput) };
|
||||
const wasmPosition = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
|
||||
? wasmMotion.state.position
|
||||
: authoritative.kinematics.pose;
|
||||
// Generic WASM-compatible cases expose their historical XYZ panel shape;
|
||||
// the selected XYZAC case is the only panel that publishes all nine pose
|
||||
// coordinates and the five inverse joints.
|
||||
const position = this.xyzacProgram
|
||||
? wasmPosition
|
||||
: { x: wasmPosition.x, y: wasmPosition.y, z: wasmPosition.z };
|
||||
const inverseJoints = this.xyzacKinematics?.snapshot?.()?.joints;
|
||||
const joints = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
|
||||
? [position.x, position.y, position.z]
|
||||
? (inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0])
|
||||
: authoritative.kinematics.joints;
|
||||
const preview = this.previewState?.status === "verified" ? this.previewState : null;
|
||||
const visual = {
|
||||
schema: "axis-visual.v1",
|
||||
pose: { x: position.x, y: position.y, z: position.z },
|
||||
pose: { ...position },
|
||||
joints: Array.isArray(joints) ? [...joints] : [],
|
||||
toolpath: (preview?.segments ?? []).map((segment, index) => ({ id: index + 1, ...segment })),
|
||||
...(includeToolpath ? { toolpath: (preview?.segments ?? []).map((segment, index) => ({ id: index + 1, ...segment })) } : {}),
|
||||
active_segment_id: wasmMotion.status === "verified" ? wasmMotion.state.active_id : authoritative.planner.active_id,
|
||||
active_progress: wasmMotion.status === "verified" ? wasmMotion.state.progress : 0,
|
||||
...(Array.isArray(motionTrace) && motionTrace.length ? { motion_trace: motionTrace } : {}),
|
||||
source: "wasm_canonical_motion",
|
||||
};
|
||||
const axisCoordinates = [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
|
||||
const unsigned = {
|
||||
schema: "axis-panel-snapshot.v1",
|
||||
context_id: this.contextId,
|
||||
@@ -171,20 +264,20 @@ export class SimSession {
|
||||
task,
|
||||
program,
|
||||
motion,
|
||||
coordinates: { machine: [position.x, position.y, position.z], relative: [position.x, position.y, position.z], commanded: [position.x, position.y, position.z] },
|
||||
coordinates: { machine: [...axisCoordinates], relative: [...axisCoordinates], commanded: [...axisCoordinates] },
|
||||
overrides: { feed: motion.feed_override, spindle: [motion.spindle_override] },
|
||||
wasm_motion: wasmMotion,
|
||||
home_limit: homeLimit,
|
||||
hal: { configuration: this.halState, runtime: halRuntime },
|
||||
devices,
|
||||
device_io: deviceIo,
|
||||
configuration: this.configurationState,
|
||||
...(includeStableRuntime ? { home_limit: homeLimit, hal: { configuration: this.halState, runtime: halRuntime }, devices, device_io: deviceIo } : {}),
|
||||
...(includeConfiguration ? { configuration: this.configurationState } : {}),
|
||||
axis_state: axis,
|
||||
gates,
|
||||
visual,
|
||||
authoritative,
|
||||
motion_authority: this.motionAuthority,
|
||||
task_authority: this.task.authority(),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
...(includeStableRuntime && this.xyzacIo ? { io: this.xyzacIo.export() } : {}),
|
||||
...(xyzac ? { xyzac } : {}),
|
||||
};
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
@@ -211,18 +304,24 @@ export class SimSession {
|
||||
}
|
||||
|
||||
commandGate(input) {
|
||||
const task = this.task.export();
|
||||
const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {};
|
||||
const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
|
||||
const devices = this.wasmDeviceProjection();
|
||||
if (input.kind === "KINEMATICS_SELECT" || input.kind === "VISUAL_CLEAR") {
|
||||
const type = Number(input.payload?.type);
|
||||
const action = input.kind === "VISUAL_CLEAR" ? "vismachClear" : type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : type === 2 ? "switchkinsUserk" : null;
|
||||
if (!action) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" };
|
||||
return xyzacGate(action, { task, program, motion, devices, xyzac: this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } : null });
|
||||
}
|
||||
const action = PANEL_GATE_ACTIONS[input.kind];
|
||||
if (!action) return { action: null, enabled: true, reason: "axis_internal_command" };
|
||||
const task = this.task.export();
|
||||
const program = this.program.loaded ? this.program.snapshot() : {};
|
||||
const motion = this.motion.export();
|
||||
const devices = this.devices.export();
|
||||
return axisGate(action, {
|
||||
task,
|
||||
program,
|
||||
motion,
|
||||
devices,
|
||||
taskfile: Boolean(this.program.loaded),
|
||||
taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().loaded : this.program.loaded),
|
||||
kinematicsIdentity: this.motionAuthority === "wasm",
|
||||
});
|
||||
}
|
||||
@@ -234,11 +333,11 @@ export class SimSession {
|
||||
tick: this.tick,
|
||||
created: this.created,
|
||||
protocol_seq: this.timeline.events.cursor,
|
||||
devices: this.devices.export(),
|
||||
motion: this.motion.export(),
|
||||
devices: this.wasmDeviceProjection({ includeBytes: true }),
|
||||
motion: this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(),
|
||||
plc: this.plc.export(),
|
||||
hal_runtime: this.hal.export(),
|
||||
device_io: this.deviceIo.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(),
|
||||
@@ -249,13 +348,15 @@ export class SimSession {
|
||||
wasm_motion: this.wasmMotion.exportState(),
|
||||
task_authority: this.task.authority(),
|
||||
wasm_home_limit: this.wasmHomeLimit.exportState(),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
preview: this.previewState,
|
||||
task: this.task.export(),
|
||||
scheduler: this.scheduler.export(),
|
||||
authoritative: this.authoritative.export(),
|
||||
authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(),
|
||||
ledger: this.ledger.export(),
|
||||
};
|
||||
if (this.program.loaded) state.program = this.program.exportState();
|
||||
if (this.xyzacProgram) state.program = { ...this.xyzacProgram.snapshot(), source_text: this.xyzacProgram.source };
|
||||
else if (this.program.loaded) state.program = this.program.exportState();
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -265,11 +366,11 @@ export class SimSession {
|
||||
session_id: this.sessionId,
|
||||
configuration: this.configurationState,
|
||||
hal: this.halState,
|
||||
devices: this.devices.export(),
|
||||
motion: this.motion.export(),
|
||||
devices: this.wasmDeviceProjection(),
|
||||
motion: this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(),
|
||||
plc: this.plc.export(),
|
||||
hal_runtime: this.hal.export(),
|
||||
device_io: this.deviceIo.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(),
|
||||
@@ -278,13 +379,15 @@ export class SimSession {
|
||||
event_cursor: this.timeline.events.cursor,
|
||||
task: this.task.export(),
|
||||
scheduler: this.scheduler.export(),
|
||||
authoritative: this.authoritative.export(),
|
||||
authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(),
|
||||
motion_authority: this.motionAuthority,
|
||||
wasm_motion: this.wasmMotion.exportState(),
|
||||
task_authority: this.task.authority(),
|
||||
wasm_home_limit: this.wasmHomeLimit.exportState(),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
};
|
||||
if (this.program.loaded) value.program = this.program.exportState();
|
||||
if (this.xyzacProgram) value.program = { ...this.xyzacProgram.snapshot(), source_text: this.xyzacProgram.source };
|
||||
else if (this.program.loaded) value.program = this.program.exportState();
|
||||
return { ...value, state_hash: fnv1a64(value) };
|
||||
}
|
||||
|
||||
@@ -325,6 +428,9 @@ export class SimSession {
|
||||
if (trace.state?.motion_authority) this.motionAuthority = trace.state.motion_authority;
|
||||
if (trace.state?.wasm_motion) this.wasmMotion.restoreState(trace.state.wasm_motion);
|
||||
if (trace.state?.wasm_home_limit) this.wasmHomeLimit.restoreState(trace.state.wasm_home_limit);
|
||||
if (trace.state?.device_io?.schema === "wasm_xyzac_device_io.v1" && this.xyzacDeviceIo) this.xyzacDeviceIo.restoreState(trace.state.device_io);
|
||||
if (trace.state?.io?.schema === "wasm_xyzac_io.v1" && this.xyzacIo) this.xyzacIo.restoreState(trace.state.io);
|
||||
if (trace.state?.kinematics && this.xyzacKinematics) this.xyzacKinematics.restoreState(trace.state.kinematics);
|
||||
if (trace.state?.motion) this.motion.restore(trace.state.motion);
|
||||
if (trace.state?.program) {
|
||||
this.program = new ProgramRunner(this.core);
|
||||
@@ -346,10 +452,62 @@ export class SimSession {
|
||||
}
|
||||
|
||||
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
|
||||
try {
|
||||
validateCheckpoint(checkpoint, { contextId: this.contextId });
|
||||
} catch (error) {
|
||||
return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ reason: String(error?.message ?? error) }] };
|
||||
}
|
||||
const replayMotionAuthority = checkpoint?.state?.motion_authority ?? this.motionAuthority;
|
||||
if (replayMotionAuthority === "wasm" && !checkpoint?.state?.wasm_motion) {
|
||||
return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ reason: "wasm_motion_checkpoint_missing" }] };
|
||||
}
|
||||
if (this.xyzacProgram && checkpoint?.state?.program?.source_text) {
|
||||
try {
|
||||
const loaded = this.xyzacProgram.load(checkpoint.state.program.source_text, checkpoint.state.program.path ?? null);
|
||||
if (loaded.status !== "verified") throw new Error(loaded.code);
|
||||
const subroutines = this.loadXyzacSubroutines(checkpoint.state.program.path ?? null);
|
||||
if (subroutines.status !== "verified") throw new Error(subroutines.code);
|
||||
const targetLine = Number.isSafeInteger(checkpoint.state.program.current_line) ? checkpoint.state.program.current_line : 0;
|
||||
const seek = this.xyzacProgram.seek(targetLine);
|
||||
if (seek.status !== "verified") throw new Error(seek.code);
|
||||
if (checkpoint.state.program) this.xyzacProgram.restoreState(checkpoint.state.program);
|
||||
if (checkpoint.state.wasm_motion) this.wasmMotion.restoreState(checkpoint.state.wasm_motion);
|
||||
if (checkpoint.state.wasm_home_limit) this.wasmHomeLimit.restoreState(checkpoint.state.wasm_home_limit);
|
||||
let restoredWasmDeviceIo = false;
|
||||
if (checkpoint.state.device_io?.schema === "wasm_xyzac_device_io.v1" && this.xyzacDeviceIo) { this.xyzacDeviceIo.restoreState(checkpoint.state.device_io); restoredWasmDeviceIo = true; }
|
||||
if (checkpoint.state.io?.schema === "wasm_xyzac_io.v1" && this.xyzacIo) this.xyzacIo.restoreState(checkpoint.state.io);
|
||||
if (checkpoint.state.devices?.schema === "wasm_xyzac_devices.v1" && this.xyzacIo) this.xyzacIo.restoreState(checkpoint.state.devices);
|
||||
if (checkpoint.state.kinematics && this.xyzacKinematics) this.xyzacKinematics.restoreState(checkpoint.state.kinematics);
|
||||
if (checkpoint.state.task) this.task.restore(checkpoint.state.task);
|
||||
if (checkpoint.state.scheduler) this.scheduler.restore(checkpoint.state.scheduler);
|
||||
if (checkpoint.state.devices?.schema !== "wasm_xyzac_devices.v1") this.devices = new VirtualDevices({ state: checkpoint.state.devices });
|
||||
this.plc = new PlcRuntime(); if (checkpoint.state.plc) this.plc.restore(checkpoint.state.plc);
|
||||
this.hal = new HalRuntime(); if (checkpoint.state.hal_runtime?.schema === "sim_hal_runtime.v1") this.hal.restore(checkpoint.state.hal_runtime);
|
||||
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId }); if (checkpoint.state.configuration_batch) this.configurationBatch.restore(checkpoint.state.configuration_batch);
|
||||
this.ioAck = new IoAckTracker(); if (checkpoint.state.io_ack) this.ioAck.restore(checkpoint.state.io_ack);
|
||||
this.callbacks = new PythonCallbacks(); if (checkpoint.state.callbacks) this.callbacks.restore(checkpoint.state.callbacks);
|
||||
this.configurationState = checkpoint.state.configuration ?? this.configurationState;
|
||||
this.halState = checkpoint.state.hal ?? this.halState;
|
||||
this.previewState = checkpoint.state.preview ?? this.previewState;
|
||||
this.python = new PythonBridge(); if (checkpoint.state.python) this.python.restoreState(checkpoint.state.python);
|
||||
this.timeline = new ReplayTimeline(checkpoint.timeline);
|
||||
if (checkpoint.state.hal_runtime?.schema === "wasm_xyzac_hal.v1" && this.xyzacIo) this.xyzacIo.halCommand({ action: "step", ticks: checkpoint.state.hal_runtime.tick ?? 0 });
|
||||
if (!restoredWasmDeviceIo && checkpoint.state.device_io?.schema === "wasm_xyzac_device_io.v1" && this.xyzacDeviceIo) this.xyzacDeviceIo.command("IO_STEP", { ticks: checkpoint.state.device_io.state?.tick ?? 0 });
|
||||
const currentOperations = [...this.operations.values()];
|
||||
this.operations = new Map([...checkpoint.operations ?? [], ...currentOperations].map((operation) => [operation.operation_id, operation]));
|
||||
const checkpointEntries = checkpoint.operations ?? [];
|
||||
const currentEntries = this.ledger.export().entries.filter((entry) => !checkpointEntries.some((operation) => operation.operation_id === entry.operation_id));
|
||||
this.ledger = new this.ledger.constructor();
|
||||
for (const operation of [...checkpointEntries, ...currentEntries]) {
|
||||
const entry = { ...operation, phase: operation.phase ?? (operation.status === "verified" ? "committed" : operation.status), side_effect_count: operation.side_effect_count ?? (operation.status === "verified" ? 1 : 0) };
|
||||
try { this.ledger.importEntry(entry); } catch { /* checkpoint operations remain available for diagnostics */ }
|
||||
}
|
||||
this.tick = checkpoint.state.tick ?? this.tick;
|
||||
return { schema: "simulation-recovery.v1", status: "recovered", context_id: this.contextId, checkpoint_hash: checkpoint.checkpoint_hash ?? null, simulation_tick: this.tick, state: this.stateForCheckpoint(), operations: checkpoint.operations ?? [] };
|
||||
} catch (error) {
|
||||
return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ code: "wasm_program_recovery", reason: String(error?.message ?? error) }] };
|
||||
}
|
||||
}
|
||||
const replayDevices = new VirtualDevices({ state: checkpoint?.state?.devices });
|
||||
const replayMotion = new MotionState();
|
||||
if (checkpoint?.state?.motion) replayMotion.restore(checkpoint.state.motion);
|
||||
@@ -546,6 +704,7 @@ export class SimSession {
|
||||
this.motionAuthority = result.state?.motion_authority ?? checkpoint?.state?.motion_authority ?? this.motionAuthority;
|
||||
if (this.motionAuthority === "wasm" && result.state?.wasm_motion) this.wasmMotion.restoreState(result.state.wasm_motion);
|
||||
if (this.motionAuthority === "wasm" && result.state?.wasm_home_limit) this.wasmHomeLimit.restoreState(result.state.wasm_home_limit);
|
||||
if (result.state?.kinematics && this.xyzacKinematics) this.xyzacKinematics.restoreState(result.state.kinematics);
|
||||
if (result.state?.task) this.task.restore(result.state.task);
|
||||
if (result.state?.scheduler) this.scheduler.restore(result.state.scheduler);
|
||||
this.authoritative = new AuthoritativeRuntime();
|
||||
@@ -556,7 +715,9 @@ export class SimSession {
|
||||
if (this.motionAuthority !== "wasm") this.authoritative.applyMotion(this.motion);
|
||||
if (this.motionAuthority === "wasm") {
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
if (wasmMotion.status !== "verified" || !result.state?.motion || fnv1a64(result.state.motion.axes) !== fnv1a64(wasmMotion.state.position)) throw new Error("wasm_motion_recovery_projection_mismatch");
|
||||
const motionAxes = result.state?.motion?.axes;
|
||||
const projectionMatches = motionAxes && Object.entries(motionAxes).every(([axis, value]) => Number.isFinite(value) && Math.abs((wasmMotion.state.position[axis] ?? Number.NaN) - value) <= 1e-12);
|
||||
if (wasmMotion.status !== "verified" || !projectionMatches) throw new Error("wasm_motion_recovery_projection_mismatch");
|
||||
}
|
||||
this.plc = new PlcRuntime();
|
||||
if (result.state?.plc) this.plc.restore(result.state.plc);
|
||||
@@ -661,8 +822,9 @@ export class SimSession {
|
||||
}
|
||||
const gate = this.commandGate(input);
|
||||
const handlerOwnsFault = (input.kind === "JOG_START" || input.kind === "JOG_STOP") && Boolean(this.motion.state.fault);
|
||||
const restartAfterCompletion = input.kind === "PROGRAM_RESTART" && this.program.status === "completed";
|
||||
if (!gate.enabled && !handlerOwnsFault && !restartAfterCompletion) {
|
||||
const restartAfterCompletion = input.kind === "PROGRAM_RESTART" && (this.xyzacProgram ? this.xyzacProgram.snapshot().status === 4 : this.program.status === "completed");
|
||||
const abortAfterCompletion = input.kind === "PROGRAM_ABORT" && (this.xyzacProgram ? this.xyzacProgram.snapshot().status === 4 : this.program.status === "completed");
|
||||
if (!gate.enabled && !handlerOwnsFault && !restartAfterCompletion && !abortAfterCompletion) {
|
||||
if (text(input.operation_id)) this.operations.delete(input.operation_id);
|
||||
const rejectedPayload = { command: input.kind, gate_reason: gate.reason, payload: input.payload ?? {} };
|
||||
try {
|
||||
@@ -713,7 +875,16 @@ export class SimSession {
|
||||
};
|
||||
|
||||
const deviceResponse = (kind, payload) => {
|
||||
const result = this.devices.command(kind, payload ?? {});
|
||||
const result = this.xyzacIo ? this.xyzacIo.command(kind, payload ?? {}) : this.devices.command(kind, payload ?? {});
|
||||
if (result.status === "verified" && this.xyzacProgram && kind === "ESTOP" && payload?.active === true) {
|
||||
// The WASM IO edge owns emergency stop. Stop the WASM trajectory and
|
||||
// pause the canonical program so no compatibility state can continue
|
||||
// feeding motion while the machine is stopped.
|
||||
const programState = this.xyzacProgram.snapshot();
|
||||
if (programState.status === 2) this.xyzacProgram.pause();
|
||||
this.wasmMotion.abort();
|
||||
this.wasmMotion.setPaused(true);
|
||||
}
|
||||
try {
|
||||
if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904);
|
||||
else if (kind === "IO_RECOVER" && result.status === "verified") this.task.recover();
|
||||
@@ -721,15 +892,17 @@ export class SimSession {
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
if (kind === "ESTOP") this.deviceIo.estop(payload?.active === true);
|
||||
if (kind === "IO_RECOVER" && result.status === "verified") this.deviceIo.interlockReset();
|
||||
if (!this.xyzacIo && kind === "ESTOP") this.deviceIo.estop(payload?.active === true);
|
||||
if (!this.xyzacIo && kind === "IO_RECOVER" && result.status === "verified") this.deviceIo.interlockReset();
|
||||
if (this.xyzacDeviceIo && kind === "ESTOP") this.xyzacDeviceIo.command("DEVICE_ESTOP", payload, input.operation_id);
|
||||
if (this.xyzacDeviceIo && kind === "IO_RECOVER" && result.status === "verified") this.xyzacDeviceIo.command("INTERLOCK_RESET", payload, input.operation_id);
|
||||
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
|
||||
return finish(this.envelope(responseKind, input, {
|
||||
...result,
|
||||
command: kind,
|
||||
task: this.task.export(),
|
||||
device_io: this.deviceIo.export(),
|
||||
motion: this.motion.export(),
|
||||
device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(),
|
||||
motion: this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(),
|
||||
operation_id: input.operation_id ?? null,
|
||||
simulation_tick: this.tick,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
@@ -737,7 +910,8 @@ export class SimSession {
|
||||
|
||||
const deviceIoResponse = (kind, payload = {}) => {
|
||||
let result;
|
||||
if (kind === "ENCODER_SET") result = this.deviceIo.encoderSet(payload.channel, payload);
|
||||
if (this.xyzacDeviceIo) result = this.xyzacDeviceIo.command(kind, payload, input.operation_id);
|
||||
else if (kind === "ENCODER_SET") result = this.deviceIo.encoderSet(payload.channel, payload);
|
||||
else if (kind === "COOLANT_COMMAND") result = this.deviceIo.coolantCommand(payload);
|
||||
else if (kind === "COOLANT_FAULT") result = this.deviceIo.coolantFault(payload.fault === true);
|
||||
else if (kind === "DOOR_SET") result = this.deviceIo.doorSet(payload.open === true);
|
||||
@@ -749,7 +923,14 @@ export class SimSession {
|
||||
else if (kind === "DEVICE_ACK") result = this.deviceIo.ack(payload.operation_id);
|
||||
else result = this.deviceIo.fail("device_io_command_unsupported");
|
||||
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
|
||||
return finish(this.envelope(responseKind, input, { ...result, command: kind, device_io: this.deviceIo.export(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
return finish(this.envelope(responseKind, input, { ...result, command: kind, device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
};
|
||||
|
||||
const visualClearResponse = () => {
|
||||
const before = { task: this.task.hash(), motion: this.wasmMotion.exportState().state_hash, program: this.xyzacProgram?.snapshot().state_hash ?? null };
|
||||
this.previewState = null;
|
||||
const after = { task: this.task.hash(), motion: this.wasmMotion.exportState().state_hash, program: this.xyzacProgram?.snapshot().state_hash ?? null };
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, visual_clear: true, visual_only: true, before, after, operation_id: input.operation_id ?? null }), "verified");
|
||||
};
|
||||
|
||||
switch (input.kind) {
|
||||
@@ -769,12 +950,21 @@ export class SimSession {
|
||||
try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
|
||||
}
|
||||
case "POWER": {
|
||||
if (input.payload?.active !== true) return finish(this.error(input, "power_state", "power_active_required"), "failed");
|
||||
const result = this.xyzacIo ? this.xyzacIo.command("IO_RECOVER", input.payload ?? {}) : this.devices.command("IO_RECOVER", input.payload ?? {});
|
||||
if (result.status === "verified") {
|
||||
try { this.task.recover(); } catch { /* READY is already powered for the selected simulation. */ }
|
||||
if (this.xyzacDeviceIo) this.xyzacDeviceIo.command("INTERLOCK_RESET", input.payload ?? {}, input.operation_id);
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, active: true, task: this.task.export(), device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "RESET": {
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "reset_while_program_active"), "failed");
|
||||
const result = this.motion.resetFault();
|
||||
if (result.status !== "verified") return finish(this.envelope("ERROR", input, { ...result, command: input.kind, task: this.task.export() }), "failed");
|
||||
if (this.task.state.state === "FAULT" || this.task.state.state === "STOPPED") this.task.recover();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { ...result, command: input.kind, task: this.task.export(), motion: this.motion.export(), operation_id: input.operation_id ?? null }));
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { ...result, command: input.kind, task: this.task.export(), motion: this.motionProjection(), operation_id: input.operation_id ?? null }));
|
||||
}
|
||||
case "LOAD_WORKSPACE": {
|
||||
const rootPath = input.payload?.root_path;
|
||||
@@ -782,6 +972,7 @@ export class SimSession {
|
||||
let startupLoaded = false;
|
||||
const configuration = input.payload?.configuration ?? null;
|
||||
if (configuration) {
|
||||
this.workspaceFiles = { ...(configuration.files ?? configuration.workspace ?? configuration.workspace_files ?? {}) };
|
||||
const startupWorkspace = configuration.workspace ?? configuration.files ?? configuration.workspace_files;
|
||||
if (startupWorkspace) {
|
||||
const startup = new ConfigurationStartupStateMachine({
|
||||
@@ -826,10 +1017,29 @@ export class SimSession {
|
||||
};
|
||||
}
|
||||
}
|
||||
this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") ? "wasm" : "js";
|
||||
this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") || configuration?.kinematics?.type === "xyzac-trt" ? "wasm" : "js";
|
||||
if (configuration?.kinematics?.type === "xyzac-trt") {
|
||||
this.xyzacKinematics?.destroy();
|
||||
this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, limits: configuration.home_limits });
|
||||
this.xyzacIo?.destroy();
|
||||
this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: 2, toolCapacity: 1001 });
|
||||
this.xyzacDeviceIo?.destroy();
|
||||
this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 });
|
||||
this.xyzacProgram?.destroy();
|
||||
this.xyzacProgram = new WasmProgramRuntime(this.core);
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
|
||||
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
}
|
||||
if (configuration?.home_limits && this.motionAuthority === "wasm") {
|
||||
this.wasmHomeLimit.destroy();
|
||||
this.wasmHomeLimit = new WasmHomeLimit(this.core, { limits: configuration.home_limits });
|
||||
this.wasmHomeLimit = new WasmHomeLimit(this.core, {
|
||||
jointCount: configuration.home_limits.joint_count ?? 3,
|
||||
kinematicsType: configuration.home_limits.kinematics_type ?? 1,
|
||||
coordinates: configuration.home_limits.coordinates ?? "XYZ",
|
||||
offsets: configuration.kinematics?.offsets ?? {},
|
||||
limits: configuration.home_limits,
|
||||
});
|
||||
}
|
||||
if (input.payload?.hal) {
|
||||
const preflight = preflightHalConfiguration(input.payload.hal);
|
||||
@@ -849,7 +1059,7 @@ export class SimSession {
|
||||
};
|
||||
}
|
||||
const configuredKinematics = input.payload?.kinematics ?? configuration?.kinematics?.type ?? null;
|
||||
if (configuredKinematics) {
|
||||
if (configuredKinematics && !(this.motionAuthority === "wasm" && configuredKinematics === "xyzac-trt")) {
|
||||
const kinematics = this.authoritative.setKinematics(configuredKinematics);
|
||||
if (kinematics.status === "blocked") return finish(this.envelope("ERROR", input, { status: "blocked", code: kinematics.code, kinematics: this.authoritative.export() }), "failed");
|
||||
}
|
||||
@@ -868,9 +1078,46 @@ export class SimSession {
|
||||
const limit = validateLimit("max_tick_step", cycles);
|
||||
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 = [];
|
||||
try {
|
||||
this.scheduler.step(cycles, {
|
||||
task: () => {
|
||||
if (this.xyzacProgram) {
|
||||
const wasmState = this.xyzacProgram.snapshot();
|
||||
if (![2, 3].includes(wasmState.status)) return;
|
||||
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);
|
||||
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}`);
|
||||
this.pendingProgramControl = 0;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const steps = this.program.tick(1);
|
||||
program_steps.push(...steps);
|
||||
for (const step of steps) {
|
||||
@@ -900,24 +1147,49 @@ export class SimSession {
|
||||
if (stepResult !== 0) throw new Error(`core_sim_step_${stepResult}`);
|
||||
this.tick = nextTick;
|
||||
this.task.setTick(nextTick);
|
||||
const motionState = this.motion.step(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);
|
||||
if (motionState.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
if (this.motionAuthority === "wasm") {
|
||||
if (wasmMotion.status === "verified") {
|
||||
const position = this.motion.applyWasmPosition(wasmMotion.state.position, wasmMotion.state.velocity, nextTick);
|
||||
if (position.status === "error") throw new Error(position.code);
|
||||
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) {
|
||||
// An empty queue is a valid fixed-tick sample during an
|
||||
// M428/M429 synchronization boundary. Record it as a
|
||||
// stationary WASM sample so the display does not invent a
|
||||
// jump across the barrier.
|
||||
const traceState = wasmMotion.status === "verified" ? wasmMotion.state : this.wasmMotion.read().state;
|
||||
const traceKinematics = this.xyzacKinematics?.snapshot?.();
|
||||
motion_trace.push({
|
||||
tick: nextTick,
|
||||
pose: { ...traceState.position },
|
||||
joints: traceKinematics?.joints?.length === 5 ? [...traceKinematics.joints] : [],
|
||||
active_segment_id: traceState.active_id,
|
||||
active_progress: traceState.progress,
|
||||
});
|
||||
}
|
||||
} else if (this.program.loaded) this.authoritative.applyMotion(this.motion);
|
||||
if (this.motionAuthority === "wasm" && this.wasmHomeLimit.read().state.homed && !motionState.fault) {
|
||||
const homeCheck = this.wasmHomeLimit.check(this.motion.state.joints);
|
||||
if (this.motionAuthority === "wasm" && this.wasmHomeLimit.read().state.homed) {
|
||||
const homeCheck = this.wasmHomeLimit.check(this.xyzacProgram ? this.wasmMotionProjection().joints : this.motion.state.joints);
|
||||
if (homeCheck.status === "error") throw new Error(homeCheck.code);
|
||||
}
|
||||
const halState = this.hal.step(nextTick);
|
||||
const halState = this.xyzacIo ? this.xyzacIo.halCommand({ action: "step", ticks: 1 }) : this.hal.step(nextTick);
|
||||
if (halState.status === "error") throw new Error(halState.code);
|
||||
const deviceIoState = this.deviceIo.step(nextTick);
|
||||
const deviceIoState = this.xyzacIo ? this.xyzacIo.command("IO_STEP", { ticks: 1 }) : this.deviceIo.step(nextTick);
|
||||
if (deviceIoState.status === "blocked") throw new Error(deviceIoState.code);
|
||||
if (this.xyzacDeviceIo) {
|
||||
const deviceStep = this.xyzacDeviceIo.command("IO_STEP", { ticks: 1 });
|
||||
if (deviceStep.status === "blocked") throw new Error(deviceStep.code);
|
||||
}
|
||||
const plcState = this.plc.scan(nextTick);
|
||||
if (plcState.status === "error") throw new Error(plcState.code);
|
||||
const ackState = this.ioAck.step(nextTick);
|
||||
@@ -932,20 +1204,38 @@ export class SimSession {
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "scheduler", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
const program = this.program.loaded ? this.program.snapshot() : null;
|
||||
const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : null;
|
||||
if (program) {
|
||||
const status = program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status;
|
||||
this.task.setProgram(status, program.line);
|
||||
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);
|
||||
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.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: 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() };
|
||||
if (program) snapshotState.program = program;
|
||||
const snapshot = this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, program_steps, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), ...(program ? { program } : {}), state_hash: snapshot.state_hash, snapshot }));
|
||||
const lightweightXyzac = Boolean(this.xyzacProgram);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, {
|
||||
status: "verified", cycles, program_steps,
|
||||
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
|
||||
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,
|
||||
}));
|
||||
}
|
||||
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) {
|
||||
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"); }
|
||||
const preview = result.status === "verified" ? this.xyzacProgram.preview() : result;
|
||||
if (result.status === "verified" && preview.status === "verified") { this.previewState = preview; this.task.setProgram("loaded", 0); }
|
||||
const finalStatus = result.status === "verified" && preview.status === "verified" ? "verified" : "error";
|
||||
return finish(this.envelope(finalStatus === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, preview, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), finalStatus);
|
||||
}
|
||||
const result = this.program.load(source, input.payload?.path ?? null);
|
||||
if (result.status === "verified") {
|
||||
this.authoritative.reset();
|
||||
@@ -962,12 +1252,17 @@ export class SimSession {
|
||||
task: this.task.export(),
|
||||
motion_authority: this.motionAuthority,
|
||||
wasm_motion: this.wasmMotion.read(),
|
||||
motion: this.motion.export(),
|
||||
motion: this.motionProjection(),
|
||||
command: input.kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
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); }
|
||||
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();
|
||||
if (result.status === "verified") {
|
||||
this.task.setMode("AUTO");
|
||||
@@ -981,13 +1276,22 @@ export class SimSession {
|
||||
task: this.task.export(),
|
||||
motion_authority: this.motionAuthority,
|
||||
wasm_motion: this.wasmMotion.read(),
|
||||
motion: this.motion.export(),
|
||||
motion: this.motionProjection(),
|
||||
command: input.kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
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) {
|
||||
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); }
|
||||
}
|
||||
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[action]();
|
||||
if (result.status === "verified") {
|
||||
if (action === "pause") {
|
||||
@@ -1008,12 +1312,19 @@ export class SimSession {
|
||||
task: this.task.export(),
|
||||
motion_authority: this.motionAuthority,
|
||||
wasm_motion: this.wasmMotion.read(),
|
||||
motion: this.motion.export(),
|
||||
motion: this.motionProjection(),
|
||||
command: input.kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "PROGRAM_RESTART": {
|
||||
if (this.xyzacProgram) {
|
||||
const source = this.xyzacProgram.source;
|
||||
const result = this.xyzacProgram.load(source, this.xyzacProgram.path);
|
||||
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(this.xyzacProgram.path); if (subroutines.status !== "verified") return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); const preview = this.xyzacProgram.preview(); if (preview.status === "verified") this.previewState = preview; this.wasmMotion.reset(); this.wasmMotion.setPaused(false); this.task.recover(); this.task.setMode("AUTO"); this.task.setProgram("loaded", 0); }
|
||||
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.restart();
|
||||
if (result.status === "verified") {
|
||||
if (["ESTOP", "FAULT"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_restart_faulted"), "failed");
|
||||
@@ -1034,6 +1345,13 @@ export class SimSession {
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "PROGRAM_STEP": {
|
||||
if (this.xyzacProgram) {
|
||||
const current = this.xyzacProgram.snapshot();
|
||||
if (current.status === 1) this.xyzacProgram.start();
|
||||
const result = this.xyzacProgram.step();
|
||||
if (result.status === "verified") { if (result.motion) this.wasmMotion.enqueue(result.motion); this.wasmMotion.setPaused(true); this.task.setProgram(result.program_status === 4 ? "completed" : "paused", result.line); this.task.transition(result.program_status === 4 ? "STOPPED" : "PAUSED"); }
|
||||
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");
|
||||
}
|
||||
if (this.task.state.state === "READY") {
|
||||
this.task.setMode("AUTO");
|
||||
this.task.transition("RUNNING");
|
||||
@@ -1059,12 +1377,17 @@ export class SimSession {
|
||||
task: this.task.export(),
|
||||
motion_authority: this.motionAuthority,
|
||||
wasm_motion: this.wasmMotion.read(),
|
||||
motion: this.motion.export(),
|
||||
motion: this.motionProjection(),
|
||||
command: input.kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "PROGRAM_SEEK": {
|
||||
if (this.xyzacProgram) {
|
||||
const result = this.xyzacProgram.seek(input.payload?.line);
|
||||
if (result.status === "verified") { this.task.setProgram("paused", input.payload?.line ?? 0); if (this.task.state.state === "READY") this.task.transition("RUNNING"); if (this.task.state.state === "RUNNING") this.task.transition("PAUSED"); }
|
||||
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.seek(input.payload?.line);
|
||||
if (result.status === "verified") this.task.setProgram("paused", result.line);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
|
||||
@@ -1077,7 +1400,28 @@ export class SimSession {
|
||||
case "MDI_EXECUTE": {
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "mdi_while_program_active"), "failed");
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
const result = this.program.executeBlock(input.payload?.block ?? input.payload?.command);
|
||||
const block = input.payload?.block ?? input.payload?.command;
|
||||
if (this.xyzacProgram) {
|
||||
const result = this.xyzacProgram.executeBlock(block);
|
||||
if (result.status === "verified") {
|
||||
const controlCode = Number(result.program?.control_code ?? 0);
|
||||
if ([428, 429, 430].includes(controlCode) && this.xyzacKinematics) {
|
||||
const mode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[controlCode];
|
||||
const selected = this.xyzacKinematics.select(mode, this.xyzacKinematics.snapshot().joints);
|
||||
if (selected.status !== "verified") return finish(this.error(input, "kinematics", selected.code ?? "xyzac_switchkins_failed"), "failed");
|
||||
result.kinematics = this.xyzacKinematics.snapshot();
|
||||
result.remap = { code: `M${controlCode}`, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" };
|
||||
}
|
||||
this.task.setMode("MDI");
|
||||
this.task.setProgram("loaded", this.xyzacProgram.snapshot().current_line);
|
||||
if (result.motion) {
|
||||
const enqueue = this.wasmMotion.enqueue(result.motion);
|
||||
if (enqueue.status === "error") return finish(this.error(input, "motion_queue", enqueue.code), "failed");
|
||||
}
|
||||
}
|
||||
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.executeBlock(block);
|
||||
if (result.status === "verified") { this.task.setMode("MDI"); this.task.setProgram("loaded", this.program.currentLine); }
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
|
||||
...result,
|
||||
@@ -1086,15 +1430,64 @@ export class SimSession {
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "KINEMATICS_SELECT": {
|
||||
if (!this.xyzacKinematics) return finish(this.error(input, "kinematics", "xyzac_kinematics_unavailable"), "failed");
|
||||
const mode = Number(input.payload?.type);
|
||||
if (![XYZAC_MODES.identity, XYZAC_MODES.xyzac, XYZAC_MODES.userk].includes(mode)) return finish(this.error(input, "kinematics", "xyzac_kinematics_type_invalid"), "failed");
|
||||
const result = this.xyzacKinematics.select(mode, this.xyzacKinematics.snapshot().joints);
|
||||
const code = mode === XYZAC_MODES.xyzac ? "M428" : mode === XYZAC_MODES.identity ? "M429" : "M430";
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "VISUAL_CLEAR":
|
||||
return visualClearResponse();
|
||||
case "HOME": {
|
||||
if (this.xyzacProgram) {
|
||||
if (this.wasmHomeLimit.read().status !== "verified") return finish(this.error(input, "home_limit", "wasm_home_limit_unavailable"), "failed");
|
||||
const home = this.wasmHomeLimit.home(input.seq);
|
||||
if (home.status === "error") return finish(this.error(input, "home_limit", home.code), "failed");
|
||||
this.task.setMode("MANUAL");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), wasm_home_limit: this.wasmHomeLimit.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
const result = this.motion.home(input.payload?.axis ?? null);
|
||||
if (result.status === "verified" && this.motionAuthority === "wasm") {
|
||||
const home = this.wasmHomeLimit.home(input.seq);
|
||||
if (home.status === "error") return finish(this.error(input, "home_limit", home.code), "failed");
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motion.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "JOG_START": case "JOG_STOP": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
|
||||
if (this.motionAuthority === "wasm") {
|
||||
if (input.kind === "JOG_STOP") {
|
||||
this.wasmMotion.abort();
|
||||
this.wasmMotion.setPaused(true);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (input.kind === "SET_OVERRIDE") {
|
||||
const feed = Number(input.payload?.feed ?? input.payload?.feed_override);
|
||||
if (!Number.isFinite(feed) || feed < 0 || feed > 2) return finish(this.error(input, "motion_override", "motion_override_range"), "failed");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, feed_override: feed, state: { ...this.motion.export(), feed_override: feed }, motion: this.motionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
|
||||
const current = this.wasmMotion.read();
|
||||
if (current.status !== "verified") return finish(this.error(input, "motion_queue", "wasm_motion_unavailable"), "failed");
|
||||
const axis = String(input.payload?.axis ?? "").toLowerCase();
|
||||
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
||||
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
|
||||
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
|
||||
this.wasmMotion.setPaused(false);
|
||||
const start = { ...current.state.position };
|
||||
const distance = Number(input.payload?.increment) > 0 ? Number(input.payload.increment) : 1;
|
||||
const target = Number(start[axis] ?? 0) + direction * distance;
|
||||
const configuredLimit = this.wasmHomeLimit.limits?.[axis];
|
||||
if (Array.isArray(configuredLimit) && configuredLimit.length === 2 && (target < Number(configuredLimit[0]) || target > Number(configuredLimit[1]))) return finish(this.error(input, "motion_queue", "motion_limit_range"), "failed");
|
||||
const end = { ...start, [axis]: target };
|
||||
const enqueue = this.wasmMotion.enqueue({ start, end, center: { x: 0, y: 0, z: 0 }, normal: { x: 0, y: 0, z: 1 }, velocity: Number(input.payload?.speed ?? 1), acceleration: 0, motion_type: 1, turn: 0, source_line: null });
|
||||
if (enqueue.status === "error") {
|
||||
const code = enqueue.code === "wasm_motion_enqueue_-602" ? "motion_limit_range" : enqueue.code === "wasm_motion_enqueue_-601" ? "motion_jog_nonfinite" : enqueue.code;
|
||||
return finish(this.error(input, "motion_queue", code), "failed");
|
||||
}
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), wasm_motion: this.wasmMotion.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
let result;
|
||||
if (input.kind === "JOG_START" && this.motionAuthority === "wasm" && !this.wasmHomeLimit.read().state.homed) return finish(this.error(input, "motion_state", "motion_unhomed"), "failed");
|
||||
if (input.kind === "JOG_START") result = this.motion.startJog(input.payload);
|
||||
@@ -1102,7 +1495,7 @@ export class SimSession {
|
||||
else if (input.kind === "SET_OVERRIDE") result = this.motion.setOverride(input.payload?.feed ?? input.payload?.feed_override, input.payload?.spindle ?? input.payload?.spindle_override);
|
||||
else result = this.motion.setFollowingError(input.payload?.value ?? input.payload?.following_error);
|
||||
if (result.status === "error" && result.state?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motion.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "SPINDLE_COMMAND": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":
|
||||
@@ -1111,6 +1504,7 @@ export class SimSession {
|
||||
case "PROBE_CONFIG": case "IO_RECOVER":
|
||||
return deviceResponse(input.kind, input.payload);
|
||||
case "LIMIT_SET": {
|
||||
if (this.xyzacProgram) return finish(this.error(input, "motion_limit", "wasm_limit_inputs_are_configuration_owned"), "failed");
|
||||
const result = this.motion.setLimit(input.payload?.axis, input.payload?.active === true);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
@@ -1125,7 +1519,11 @@ export class SimSession {
|
||||
case "HAL_COMMAND": case "HAL_SET": {
|
||||
const payload = input.payload ?? {};
|
||||
let result;
|
||||
if (input.kind === "HAL_SET") {
|
||||
if (this.xyzacIo && input.kind === "HAL_SET") {
|
||||
result = this.xyzacIo.halValue(payload.name, payload.value, payload.type === "bit" ? 1 : payload.type === "s32" ? 2 : payload.type === "u32" ? 3 : 4, payload.target === "param");
|
||||
} else if (this.xyzacIo) {
|
||||
result = this.xyzacIo.halCommand(payload);
|
||||
} else if (input.kind === "HAL_SET") {
|
||||
result = payload.target === "param" ? this.hal.setParam(payload.name, payload.value, payload.type) : this.hal.setPin(payload.name, payload.value, payload.type);
|
||||
} else if (payload.action === "configure") {
|
||||
result = this.hal.configure(payload);
|
||||
@@ -1142,7 +1540,7 @@ export class SimSession {
|
||||
} else {
|
||||
result = this.hal.fail("hal_command_unsupported");
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), hal_runtime: this.hal.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "ENCODER_SET": case "COOLANT_COMMAND": case "COOLANT_FAULT": case "DOOR_SET": case "INTERLOCK_SET": case "INTERLOCK_RESET": case "DEVICE_ESTOP": case "DEVICE_ACK_ISSUE": case "DEVICE_ACK": case "DEVICE_ACK_ABORT":
|
||||
return deviceIoResponse(input.kind, input.payload);
|
||||
@@ -1162,6 +1560,10 @@ export class SimSession {
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, io_ack: this.ioAck.export(), device_io: this.deviceIo.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "REMAP_REGISTER": case "REMAP_CALL": case "VCP_REGISTER": case "VCP_UPDATE": case "FILTER_EXEC": {
|
||||
if (this.xyzacProgram && input.kind === "VCP_UPDATE" && ["type0-button", "type1-button", "type2-button"].includes(input.payload?.pin)) return finish(this.error(input, "pyvcp_switch_requires_mdi", "pyvcp_switch_requires_mdi"), "failed");
|
||||
if (this.xyzacProgram && input.kind === "VCP_UPDATE" && input.payload?.pin === "vismach-clear") {
|
||||
return visualClearResponse();
|
||||
}
|
||||
const result = input.kind === "REMAP_REGISTER"
|
||||
? this.callbacks.registerRemap(input.payload?.name, input.payload)
|
||||
: input.kind === "REMAP_CALL"
|
||||
@@ -1174,6 +1576,7 @@ export class SimSession {
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, callbacks: this.callbacks.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "ESTOP": {
|
||||
if (this.xyzacProgram) return deviceResponse(input.kind, input.payload);
|
||||
const motionResult = this.motion.estop(input.payload?.active === true);
|
||||
const deviceResult = deviceResponse(input.kind, input.payload);
|
||||
if (motionResult.status === "error") return finish(this.envelope("ERROR", input, { ...motionResult, task: this.task.export() }), "failed");
|
||||
@@ -1207,6 +1610,17 @@ export class SimSession {
|
||||
const result = this.restoreCheckpoint(input.payload?.checkpoint, input.payload?.events, input.payload?.reconcile);
|
||||
return finish(this.envelope("RECOVERY_STATUS", input, result), result.status === "blocked" ? "failed" : "verified");
|
||||
}
|
||||
case "CANCEL": {
|
||||
const target = input.payload?.operation_id;
|
||||
let ledgerResult = null;
|
||||
if (text(target)) {
|
||||
try { ledgerResult = this.ledger.abort(target, input.payload?.reason ?? "cancel_requested"); }
|
||||
catch (error) { ledgerResult = { status: "missing", error: String(error?.message ?? error) }; }
|
||||
const operation = this.operations.get(target);
|
||||
if (operation && operation.status !== "verified") operation.status = "aborted";
|
||||
}
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: ledgerResult?.status ?? "missing", cancelled: ledgerResult?.status === "aborted", retry_allowed: false, operation_id: target ?? null, ledger: ledgerResult }));
|
||||
}
|
||||
case "ABORT": {
|
||||
const target = input.payload?.operation_id;
|
||||
const operation = text(target) ? this.operations.get(target) : null;
|
||||
@@ -1222,6 +1636,9 @@ export class SimSession {
|
||||
}
|
||||
case "DESTROY":
|
||||
this.program.destroy();
|
||||
this.xyzacIo?.destroy();
|
||||
this.xyzacDeviceIo?.destroy();
|
||||
this.xyzacKinematics?.destroy();
|
||||
this.task.destroy();
|
||||
this.scheduler.destroy();
|
||||
call(this.core, "sim_destroy");
|
||||
|
||||
Reference in New Issue
Block a user