Files
linuxcncWebWASM/web/worker/sim-session.mjs

2010 lines
137 KiB
JavaScript

import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
import { capabilities, validateLimit } from "./capabilities.mjs";
import { mapCoreError } from "./error-map.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";
import { DEVICE_COMMANDS, VirtualDevices } from "./virtual-devices.mjs";
import { PythonBridge } from "./python-bridge.mjs";
import { ProgramRunner } from "./program-runner.mjs";
import { SimTask } from "./sim-task.mjs";
import { SimScheduler, SCHEDULER_PHASES } from "./sim-scheduler.mjs";
import { OperationLedger } from "./operation-ledger.mjs";
import { MotionState } from "./motion-state.mjs";
import { PlcRuntime } from "./plc-runtime.mjs";
import { HalRuntime } from "./hal-runtime.mjs";
import { DeviceIoRuntime } from "./device-io.mjs";
import { IoAckTracker } from "./io-ack.mjs";
import { PythonCallbacks } from "./python-callbacks.mjs";
import { createTrace, validateTrace } from "./trace-export.mjs";
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 { WasmHomeLimit } from "./wasm-home-limit.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";
const OPERATION_KINDS = new Set([
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP",
"HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "TOOL_PREPARE",
"TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
"SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER",
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET",
"ENCODER_SET", "COOLANT_COMMAND", "COOLANT_FAULT", "DOOR_SET", "INTERLOCK_SET", "INTERLOCK_RESET", "DEVICE_ESTOP", "DEVICE_ACK_ISSUE", "DEVICE_ACK", "DEVICE_ACK_ABORT",
"CONFIGURATION_BATCH",
"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({
PROGRAM_RUN: "run", PROGRAM_STEP: "step", PROGRAM_PAUSE: "pause",
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",
});
const WASM_GATE_COMMANDS = Object.freeze({
ESTOP: 1, RESET: 2, POWER: 3, HOME: 4, JOG: 5, MDI: 6, RUN: 7,
PAUSE: 8, RESUME: 9, ABORT: 10, RESTART: 11, KINEMATICS: 12,
SET_MODE: 13, PROGRAM_LOAD: 14, STEP: 15, OVERRIDE: 16,
});
const WASM_GATE_REASONS = Object.freeze({
1: "xyzac_estop_active", 2: "xyzac_machine_off", 3: "xyzac_power_already_on",
4: "xyzac_interp_running", 5: "axis_taskfile_missing", 6: "linuxcnc_wasm_estop_inactive",
7: "xyzac_pause_state_gate", 8: "xyzac_resume_state_gate", 9: "xyzac_abort_state_gate",
10: "xyzac_restart_state_gate", 11: "xyzac_kinematics_type_invalid", 12: "linuxcnc_wasm_mode_unavailable",
13: "linuxcnc_wasm_auto_interp_active", 14: "xyzac_jog_state_gate", 15: "xyzac_mdi_queue_full",
16: "xyzac_reset_state_gate", 17: "linuxcnc_wasm_gate_unknown_command", 18: "xyzac_not_all_homed",
});
function text(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
function call(core, name, ...args) {
if (args.some((value) => typeof value === "string") && typeof core.ccall === "function") {
return core.ccall(name, "number", args.map((value) => typeof value === "string" ? "string" : "number"), args);
}
const direct = core[`_${name}`] ?? core[name];
if (typeof direct === "function") return direct(...args);
if (typeof core.cwrap === "function") {
const wrapped = core.cwrap(name, "number", args.map((value) => typeof value === "string" ? "string" : "number"));
return wrapped(...args);
}
throw new Error(`missing core export: ${name}`);
}
function appendCoreEvent(core, seq, tick, kind, payload) {
if (typeof core.ccall !== "function") return 0;
const bytes = new TextEncoder().encode(canonicalPayload(payload));
return core.ccall("sim_event_append", "number", ["number", "number", "string", "array", "number"], [seq, tick, kind, bytes, bytes.length]);
}
function canonicalPayload(value) {
if (value === null || typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(canonicalPayload).join(",")}]`;
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalPayload(value[key])}`).join(",")}}`;
}
export class SimSession {
constructor(core, { contextId = CONTEXT_ID, sessionId = "" } = {}) {
this.core = core;
this.contextId = contextId;
this.sessionId = sessionId;
this.tick = 0;
this.created = false;
this.timeline = new ReplayTimeline();
this.operations = new Map();
this.ledger = new OperationLedger();
this.devices = new VirtualDevices();
this.motion = new MotionState();
this.plc = new PlcRuntime();
this.hal = new HalRuntime();
this.deviceIo = new DeviceIoRuntime();
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: contextId });
this.ioAck = new IoAckTracker();
this.callbacks = new PythonCallbacks();
this.python = new PythonBridge();
this.program = new ProgramRunner(core);
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.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {});
const task = statePayload.task ?? this.task.export();
// 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 currentMotion = 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 compactProgram = motionDelta ? {
schema: program.schema,
source: program.source,
status: program.status,
program_status: program.program_status,
loaded: program.loaded,
current_line: program.current_line,
line: program.line,
execution_line: program.execution_line,
execution_line_text: program.execution_line_text,
path: program.path,
source_hash: program.source_hash,
state_hash: program.state_hash,
active_gcodes: program.active_gcodes,
} : null;
const trace = Array.isArray(statePayload.motion_trace) ? statePayload.motion_trace : [];
const motionSample = motionDelta ? (trace.at(-1) ?? {
tick,
pose: currentMotion.state?.position ?? null,
joints: [],
active_segment_id: currentMotion.state?.active_id ?? 0,
active_progress: currentMotion.state?.progress ?? 0,
}) : null;
const responsePayload = motionDelta ? {
status: statePayload.status,
cycles: statePayload.cycles,
motion_trace: trace,
execution_trace: Array.isArray(statePayload.execution_trace) ? statePayload.execution_trace : [],
motion_sample: motionSample,
snapshot_mode: "motion_delta",
motion_authority: statePayload.motion_authority ?? this.motionAuthority,
task,
task_authority: statePayload.task_authority ?? this.task.authority(),
wasm_motion: statePayload.wasm_motion ?? currentMotion,
motion: statePayload.motion ?? this.motionProjection(),
program: compactProgram,
state_hash: statePayload.state_hash ?? null,
} : statePayload;
return {
schema_version: PROTOCOL_SCHEMA,
kind,
context_id: this.contextId,
session_id: this.sessionId,
seq: input.seq,
...(text(input.operation_id) ? { operation_id: input.operation_id } : {}),
simulation_tick: tick,
payload: {
...responsePayload,
...(panelSnapshot ? { panel_snapshot: panelSnapshot } : {}),
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 ?? {} }),
},
};
}
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: this.wasmMotion.controls?.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 };
}
resetXyzacProgramToLoaded() {
if (!this.xyzacProgram?.source) return { status: "verified" };
const source = this.xyzacProgram.source;
const path = this.xyzacProgram.path;
const loaded = this.xyzacProgram.load(source, path);
if (loaded.status !== "verified") return loaded;
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
const subroutines = this.loadXyzacSubroutines(path);
if (subroutines.status !== "verified") return subroutines;
const preview = this.xyzacProgram.preview();
if (preview.status !== "verified") return preview;
this.previewState = preview;
return { status: "verified" };
}
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 ioState = this.xyzacIo?.canonical?.() ?? {};
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 ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, action: input.kind, payload: input.payload ?? {} });
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, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, 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 = this.xyzacProgram
? {
load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }),
run: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }),
runLine: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }),
step: this.xyzacCommandGate({ kind: "PROGRAM_STEP", payload: {} }),
pause: this.xyzacCommandGate({ kind: "PROGRAM_PAUSE", payload: {} }),
resume: this.xyzacCommandGate({ kind: "PROGRAM_RESUME", payload: {} }),
abort: this.xyzacCommandGate({ kind: "PROGRAM_ABORT", payload: {} }),
restart: this.xyzacCommandGate({ kind: "PROGRAM_RESTART", payload: {} }),
mdi: this.xyzacCommandGate({ kind: "MDI_EXECUTE", payload: {} }),
jogStart: this.xyzacCommandGate({ kind: "JOG_START", payload: {} }),
jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }),
home: this.xyzacCommandGate({ kind: "HOME", payload: {} }),
override: this.xyzacCommandGate({ kind: "SET_OVERRIDE", payload: { feed: 1 } }),
estop: this.xyzacCommandGate({ kind: "ESTOP", payload: { active: true } }),
recover: this.xyzacCommandGate({ kind: "IO_RECOVER", payload: {} }),
power: this.xyzacCommandGate({ kind: "POWER", payload: { active: ioState.estop ? false : !Boolean(ioState.ready) } }),
powerOn: this.xyzacCommandGate({ kind: "POWER", payload: { active: true } }),
powerOff: this.xyzacCommandGate({ kind: "POWER", payload: { active: false } }),
reset: this.xyzacCommandGate({ kind: "RESET", payload: {} }),
...Object.fromEntries([0, 1, 2].map((type) => [type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : "switchkinsUserk", this.xyzacCommandGate({ kind: "KINEMATICS_SELECT", payload: { type } })])),
vismachClear: { action: "vismachClear", enabled: true, reason: "axis_visual_only", visual_only: true },
}
: { ...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"
? (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: { ...position },
joints: Array.isArray(joints) ? [...joints] : [],
...(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,
session_id: this.sessionId,
simulation_tick: tick,
task,
program,
motion,
coordinates: { machine: [...axisCoordinates], relative: [...axisCoordinates], commanded: [...axisCoordinates] },
overrides: { feed: motion.feed_override, spindle: [motion.spindle_override] },
wasm_motion: wasmMotion,
...(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) };
}
error(input, code, messageKey, retryable = false) {
return this.envelope("ERROR", input, {
status: "error",
code,
message_key: messageKey,
retryable,
operation_id: input.operation_id ?? null,
simulation_tick: this.tick,
});
}
coreError(input, result, source) {
const mapped = mapCoreError(result, { operation_id: input.operation_id, simulation_tick: this.tick, source });
return this.envelope("ERROR", input, mapped);
}
operationState(input) {
if (!text(input.operation_id) || !OPERATION_KINDS.has(input.kind)) return null;
return this.operations.get(input.operation_id) ?? null;
}
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();
// The selected XYZAC case is governed by the native WASM task/IO/program
// handlers. JS only receives the result; it must not pre-reject a command
// using a duplicate gate implementation.
if (this.xyzacProgram && input.kind !== "VISUAL_CLEAR") return this.xyzacCommandGate(input);
if (input.kind === "KINEMATICS_SELECT" || input.kind === "VISUAL_CLEAR") {
if (input.kind === "KINEMATICS_SELECT" && ![0, 1, 2].includes(Number(input.payload?.type))) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" };
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" };
return axisGate(action, {
task,
program,
motion,
devices,
taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().loaded : this.program.loaded),
kinematicsIdentity: this.motionAuthority === "wasm",
});
}
xyzacCommandGate(input) {
const task = this.task.export();
const program = this.xyzacProgram.snapshot();
const io = this.xyzacIo?.canonical?.() ?? {};
const wasmMotion = this.wasmMotion.read();
const homeLimit = this.wasmHomeLimit.read();
const estop = Boolean(io.estop);
const ready = Boolean(io.ready) && !estop;
const loaded = program.status !== 0;
const action = PANEL_GATE_ACTIONS[input.kind] ?? null;
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" ? WASM_GATE_COMMANDS.JOG
: input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
: input.kind === "PROGRAM_LOAD" ? WASM_GATE_COMMANDS.PROGRAM_LOAD
: input.kind === "PROGRAM_RESTART" ? WASM_GATE_COMMANDS.RESTART
: input.kind === "PROGRAM_RUN" ? WASM_GATE_COMMANDS.RUN
: input.kind === "PROGRAM_PAUSE" ? WASM_GATE_COMMANDS.PAUSE
: input.kind === "PROGRAM_RESUME" ? WASM_GATE_COMMANDS.RESUME
: input.kind === "PROGRAM_ABORT" ? WASM_GATE_COMMANDS.ABORT
: input.kind === "PROGRAM_STEP" ? WASM_GATE_COMMANDS.STEP
: input.kind === "MDI_EXECUTE" ? WASM_GATE_COMMANDS.MDI
: input.kind === "KINEMATICS_SELECT" ? WASM_GATE_COMMANDS.KINEMATICS
: input.kind === "SET_MODE" ? WASM_GATE_COMMANDS.SET_MODE
: input.kind === "RESET" ? WASM_GATE_COMMANDS.RESET
: input.kind === "HOME" ? WASM_GATE_COMMANDS.HOME
: input.kind === "ESTOP" ? WASM_GATE_COMMANDS.ESTOP
: input.kind === "POWER" ? WASM_GATE_COMMANDS.POWER
: input.kind === "IO_RECOVER" ? WASM_GATE_COMMANDS.POWER : null;
if (!command) return { action, enabled: true, reason: "linuxcnc_wasm_authority" };
const requested = input.kind === "POWER" || input.kind === "ESTOP" ? (input.payload?.active === true ? 1 : 0)
: input.kind === "KINEMATICS_SELECT" ? Number(input.payload?.type)
: input.kind === "SET_MODE" ? ({ MANUAL: 0, MDI: 1, AUTO: 2 }[String(input.payload?.mode ?? "").toUpperCase()] ?? 3) : 0;
const result = this.task.commandGate(command, {
requested,
ioReady: ready,
ioEstop: estop,
homed: Boolean(homeLimit.state?.homed),
queueDepth: Number(task.queued_mdi_commands ?? 0),
queueLimit: Number(task.max_queued_mdi_commands ?? 10),
});
if (result === null) return { action, enabled: false, reason: "linuxcnc_wasm_gate_unavailable", taskState: task.state, interpState: "UNKNOWN" };
let reason = result === 0 ? "linuxcnc_wasm_authority" : WASM_GATE_REASONS[result] ?? "linuxcnc_wasm_gate_unknown_command";
if (result === 2 && input.kind === "POWER" && input.payload?.active !== true) reason = "xyzac_power_already_off";
if (result === 3 && input.kind === "POWER") reason = "xyzac_power_already_on";
if (result === 4 && input.kind === "MDI_EXECUTE") reason = "mdi_while_program_active";
if (result === 4 && input.kind === "PROGRAM_STEP") reason = "xyzac_interp_running";
if (result === 5 && input.kind === "PROGRAM_RESTART") reason = "xyzac_taskfile_missing";
return { action, enabled: result === 0, reason, taskState: task.state, interpState: task.state === "PAUSED" || program.status === 3 ? "PAUSED" : task.state === "RUNNING" || program.status === 2 ? "READING" : "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status, queue_depth: wasmMotion.state.queue_depth, gate_result: result } };
}
stateForCheckpoint() {
const state = {
context_id: this.contextId,
session_id: this.sessionId,
tick: this.tick,
created: this.created,
protocol_seq: this.timeline.events.cursor,
devices: this.wasmDeviceProjection({ includeBytes: true }),
motion: this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(),
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(),
configuration: this.configurationState,
hal: this.halState,
python: this.python.exportState(),
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() } : {}),
preview: this.previewState,
task: this.task.export(),
scheduler: this.scheduler.export(),
authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(),
ledger: this.ledger.export(),
};
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;
}
workspacePackageState() {
const value = {
context_id: this.contextId,
session_id: this.sessionId,
configuration: this.configurationState,
hal: this.halState,
devices: this.wasmDeviceProjection(),
motion: this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(),
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(),
python: this.python.exportState(),
tick: this.tick,
event_cursor: this.timeline.events.cursor,
task: this.task.export(),
scheduler: this.scheduler.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.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) };
}
createCheckpoint() {
const latest = this.timeline.snapshots.latest();
if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) {
this.timeline.checkpoint(
this.timeline.events.cursor,
this.timeline.events.tick,
this.stateForCheckpoint(),
latest?.snapshot_id ?? null,
);
}
return createCheckpoint({
contextId: this.contextId,
timeline: this.timeline,
state: this.stateForCheckpoint(),
operations: [...this.operations.values()],
});
}
createPowerOffCheckpoint() {
const checkpoint = this.createCheckpoint();
const state = structuredClone(checkpoint.state);
// Operation history and replay snapshots are useful for an explicit
// recovery command, but are redundant for a power-off restart. Omitting
// them keeps the browser durable record bounded during long JOG runs.
delete state.ledger;
const compact = {
schema: "xyzac-power-off-checkpoint.v1",
context_id: this.contextId,
simulation_tick: checkpoint.simulation_tick,
state,
state_hash: fnv1a64(state),
};
return { ...compact, checkpoint_hash: fnv1a64(compact) };
}
restoreCompactPowerOff(record, transportSeq) {
if (!record || record.schema !== "xyzac-power-off-checkpoint.v1" || record.context_id !== this.contextId || !record.state) return { status: "blocked", code: "runtime_checkpoint_schema", reason: "power_off_checkpoint_schema_invalid" };
if (fnv1a64(record.state) !== record.state_hash || fnv1a64(Object.fromEntries(Object.entries(record).filter(([key]) => key !== "checkpoint_hash"))) !== record.checkpoint_hash) return { status: "blocked", code: "runtime_checkpoint_hash", reason: "power_off_checkpoint_hash_mismatch" };
if (!Number.isSafeInteger(transportSeq) || transportSeq < 1 || !Number.isSafeInteger(record.simulation_tick) || record.simulation_tick < 0) return { status: "blocked", code: "runtime_checkpoint_range", reason: "power_off_checkpoint_range_invalid" };
const state = record.state;
const liveTimeline = this.timeline.export();
const liveLedger = this.ledger;
const liveOperations = [...this.operations.values()];
try {
if (!this.xyzacProgram || !state.program?.source_text) throw new Error("power_off_program_missing");
const loaded = this.xyzacProgram.load(state.program.source_text, state.program.path ?? null);
if (loaded.status !== "verified") throw new Error(loaded.code);
const subroutines = this.loadXyzacSubroutines(state.program.path ?? null);
if (subroutines.status !== "verified") throw new Error(subroutines.code);
if (this.xyzacProgram.restoreState(state.program).status === "error") throw new Error("power_off_program_restore_failed");
if (state.wasm_motion) this.wasmMotion.restoreState(state.wasm_motion);
if (state.wasm_home_limit) this.wasmHomeLimit.restoreState(state.wasm_home_limit);
if (state.device_io && this.xyzacDeviceIo) this.xyzacDeviceIo.restoreState(state.device_io);
if (state.devices && this.xyzacIo) this.xyzacIo.restoreState(state.devices);
if (state.kinematics && this.xyzacKinematics) this.xyzacKinematics.restoreState(state.kinematics);
if (state.task) {
const task = { ...state.task, session_id: this.sessionId, seq: transportSeq, simulation_tick: record.simulation_tick };
delete task.state_hash;
task.state_hash = fnv1a64(task);
this.task.restore(task);
}
if (state.scheduler) this.scheduler.restore(state.scheduler);
this.plc = new PlcRuntime(); if (state.plc) this.plc.restore(state.plc);
this.hal = new HalRuntime(); if (state.hal_runtime?.schema === "sim_hal_runtime.v1") this.hal.restore(state.hal_runtime);
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId }); if (state.configuration_batch) this.configurationBatch.restore(state.configuration_batch);
this.ioAck = new IoAckTracker(); if (state.io_ack) this.ioAck.restore(state.io_ack);
this.callbacks = new PythonCallbacks(); if (state.callbacks) this.callbacks.restore(state.callbacks);
this.configurationState = state.configuration ?? this.configurationState;
this.halState = state.hal ?? this.halState;
this.previewState = state.preview ?? this.previewState;
this.python = new PythonBridge(); if (state.python) this.python.restoreState(state.python);
const liveEvents = liveTimeline.events.events;
const rebasedEvents = liveEvents.map((event, index) => index === liveEvents.length - 1 ? { ...event, tick: record.simulation_tick } : event);
this.timeline = new ReplayTimeline({ schema: liveTimeline.schema, events: { ...liveTimeline.events, tick: record.simulation_tick, events: rebasedEvents }, snapshots: { schema: "snapshot.v1", snapshots: [] } });
this.tick = record.simulation_tick;
const protocolTick = call(this.core, "sim_protocol_set_tick", record.simulation_tick);
if (protocolTick !== 0) throw new Error(`protocol_tick_${protocolTick}`);
this.timeline.checkpoint(transportSeq, record.simulation_tick, this.stateForCheckpoint(), null);
this.operations = new Map(liveOperations.map((operation) => [operation.operation_id, operation]));
this.ledger = liveLedger;
return { status: "verified", storage: "browser_client_storage", checkpoint_hash: record.checkpoint_hash, simulation_tick: record.simulation_tick, rebased_seq: transportSeq };
} catch (error) {
this.ledger = liveLedger;
this.operations = new Map(liveOperations.map((operation) => [operation.operation_id, operation]));
return { status: "blocked", code: "runtime_checkpoint_restore_failed", reason: String(error?.message ?? error) };
}
}
restorePersistedPowerOff(checkpoint, transportSeq) {
if (!checkpoint || !Number.isSafeInteger(transportSeq) || transportSeq < 1) return { status: "blocked", code: "runtime_checkpoint_input_invalid", reason: "checkpoint_or_sequence_invalid" };
if (checkpoint.schema === "xyzac-power-off-checkpoint.v1") return this.restoreCompactPowerOff(checkpoint, transportSeq);
const liveTimeline = this.timeline.export();
const liveLedger = this.ledger;
const liveOperations = [...this.operations.values()];
// A browser restart creates a new transport session id. Adapt only the
// task identity and recompute checkpoint digests before restoring so the
// WASM task canonical (which was initialized with the new id) remains
// self-consistent.
let checkpointForRestore = checkpoint;
if (checkpoint.state?.task?.session_id && checkpoint.state.task.session_id !== this.sessionId) {
checkpointForRestore = structuredClone(checkpoint);
checkpointForRestore.state.task.session_id = this.sessionId;
checkpointForRestore.state.task.state_hash = fnv1a64(Object.fromEntries(Object.entries(checkpointForRestore.state.task).filter(([key]) => key !== "state_hash")));
checkpointForRestore.state_hash = fnv1a64(checkpointForRestore.state);
const unsigned = Object.fromEntries(Object.entries(checkpointForRestore).filter(([key]) => key !== "checkpoint_hash"));
checkpointForRestore.checkpoint_hash = fnv1a64(unsigned);
}
const restored = this.restoreCheckpoint(checkpointForRestore);
if (restored.status !== "recovered") return restored;
const restoredTick = this.tick;
const restoredTask = this.task.export();
const rebasedTask = { ...restoredTask, session_id: this.sessionId, seq: transportSeq, simulation_tick: restoredTick };
delete rebasedTask.state_hash;
rebasedTask.state_hash = fnv1a64(rebasedTask);
try {
this.task.restore(rebasedTask);
const liveEvents = liveTimeline.events.events;
const rebasedEvents = liveEvents.map((event, index) => index === liveEvents.length - 1 ? { ...event, tick: restoredTick } : event);
this.timeline = new ReplayTimeline({
schema: liveTimeline.schema,
events: { ...liveTimeline.events, tick: restoredTick, events: rebasedEvents },
snapshots: { schema: "snapshot.v1", snapshots: [] },
});
this.tick = restoredTick;
const protocolTick = call(this.core, "sim_protocol_set_tick", restoredTick);
if (protocolTick !== 0) throw new Error(`protocol_tick_${protocolTick}`);
this.timeline.checkpoint(transportSeq, restoredTick, this.stateForCheckpoint(), null);
this.operations = new Map(liveOperations.map((operation) => [operation.operation_id, operation]));
this.ledger = liveLedger;
return { status: "verified", storage: "browser_client_storage", checkpoint_hash: checkpoint.checkpoint_hash, simulation_tick: restoredTick, rebased_seq: transportSeq };
} catch (error) {
return { status: "blocked", code: "runtime_checkpoint_rebase_failed", reason: String(error?.message ?? error) };
}
}
exportTrace() {
const latest = this.timeline.snapshots.latest();
if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) {
this.timeline.checkpoint(this.timeline.events.cursor, this.timeline.events.tick, this.stateForCheckpoint(), latest?.snapshot_id ?? null);
}
return createTrace({ contextId: this.contextId, timeline: this.timeline, state: this.stateForCheckpoint(), task: this.task.export(), tick: this.tick });
}
importTrace(trace) {
const validated = validateTrace(trace, { contextId: this.contextId });
this.timeline = validated.timeline;
this.created = Boolean(trace.state?.created);
if (trace.state?.ledger?.entries) this.ledger = new OperationLedger(trace.state.ledger.entries);
if (trace.state?.task) this.task.restore(trace.state.task);
if (trace.state?.scheduler) this.scheduler.restore(trace.state.scheduler);
if (trace.state?.authoritative) this.authoritative.restore(trace.state.authoritative);
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);
this.program.restoreState(trace.state.program);
}
if (trace.state?.configuration !== undefined) this.configurationState = trace.state.configuration;
if (trace.state?.hal !== undefined) this.halState = trace.state.hal;
if (trace.state?.preview !== undefined) this.previewState = trace.state.preview;
if (trace.state?.plc) this.plc.restore(trace.state.plc);
if (trace.state?.hal_runtime) this.hal.restore(trace.state.hal_runtime);
if (trace.state?.device_io) this.deviceIo.restore(trace.state.device_io);
if (trace.state?.configuration_batch) this.configurationBatch.restore(trace.state.configuration_batch);
if (trace.state?.io_ack) this.ioAck.restore(trace.state.io_ack);
if (trace.state?.callbacks) this.callbacks.restore(trace.state.callbacks);
if (trace.state?.python) this.python.restoreState(trace.state.python);
if (trace.state?.devices) this.devices = new VirtualDevices({ state: trace.state.devices });
this.tick = trace.tick;
return { status: "verified", trace_hash: trace.trace_hash, state_hash: fnv1a64(trace.state), seq: trace.seq, tick: trace.tick };
}
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);
const replayPlc = new PlcRuntime();
if (checkpoint?.state?.plc) replayPlc.restore(checkpoint.state.plc);
const replayHal = new HalRuntime();
if (checkpoint?.state?.hal_runtime) replayHal.restore(checkpoint.state.hal_runtime);
const replayDeviceIo = new DeviceIoRuntime();
if (checkpoint?.state?.device_io) replayDeviceIo.restore(checkpoint.state.device_io);
const replayConfigurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId });
if (checkpoint?.state?.configuration_batch) replayConfigurationBatch.restore(checkpoint.state.configuration_batch);
const replayIoAck = new IoAckTracker();
if (checkpoint?.state?.io_ack) replayIoAck.restore(checkpoint.state.io_ack);
const replayCallbacks = new PythonCallbacks();
if (checkpoint?.state?.callbacks) replayCallbacks.restore(checkpoint.state.callbacks);
const replayProgram = new ProgramRunner(this.core);
if (checkpoint?.state?.program) replayProgram.restoreState(checkpoint.state.program);
const replayPython = new PythonBridge();
if (checkpoint?.state?.python) replayPython.restoreState(checkpoint.state.python);
const replayAuthoritative = new AuthoritativeRuntime();
if (checkpoint?.state?.authoritative) replayAuthoritative.restore(checkpoint.state.authoritative);
let replayWasmMotion = null;
let replayHomeLimit = null;
try {
if (replayMotionAuthority === "wasm") {
replayWasmMotion = new WasmMotionQueue(this.core);
replayWasmMotion.restoreState(checkpoint.state.wasm_motion);
if (checkpoint.state.wasm_home_limit) {
replayHomeLimit = new WasmHomeLimit(this.core);
replayHomeLimit.restoreState(checkpoint.state.wasm_home_limit);
}
}
} 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 result = recoverSimulation({
checkpoint,
contextId: this.contextId,
events,
reconcile,
apply: (state, event) => {
if (DEVICE_COMMANDS.has(event.kind)) replayDevices.applyEvent(event.kind, event.payload);
if (event.kind === "HOME") {
replayMotion.home(event.payload?.axis ?? null);
const home = replayHomeLimit?.home(event.seq);
if (home?.status === "error") throw new Error(home.code);
}
if (event.kind === "JOG_START") replayMotion.startJog(event.payload);
if (event.kind === "JOG_STOP") replayMotion.stopJog();
if (event.kind === "SET_OVERRIDE") replayMotion.setOverride(event.payload?.feed ?? event.payload?.feed_override, event.payload?.spindle ?? event.payload?.spindle_override);
if (event.kind === "LIMIT_SET") replayMotion.setLimit(event.payload?.axis, event.payload?.active === true);
if (event.kind === "ESTOP") replayMotion.estop(event.payload?.active === true);
if (event.kind === "ESTOP") replayDeviceIo.estop(event.payload?.active === true);
if (event.kind === "IO_RECOVER") replayDeviceIo.interlockReset();
if (event.kind === "SIM_STEP") replayMotion.step(event.tick);
if (event.kind === "PROGRAM_LOAD") replayProgram.load(event.payload?.source ?? event.payload?.text ?? event.payload?.program_source, event.payload?.path ?? null);
if (event.kind === "PROGRAM_RUN") { replayProgram.start(); replayWasmMotion?.setPaused(false); }
if (event.kind === "PROGRAM_PAUSE") { replayProgram.pause(); replayWasmMotion?.setPaused(true); }
if (event.kind === "PROGRAM_RESUME") { replayProgram.resume(); replayWasmMotion?.setPaused(false); }
if (event.kind === "PROGRAM_ABORT") { replayProgram.abort(); replayWasmMotion?.abort(); }
if (event.kind === "PROGRAM_RESTART") { replayProgram.restart(); replayWasmMotion?.reset(); }
if (event.kind === "SIM_STEP" && replayProgram.loaded) {
// PROGRAM_LOAD resets the planner clock, while the session clock is
// monotonic. Recovery must start this replay at the checkpoint tick
// or one logical step would consume the whole historical interval.
if (replayMotionAuthority === "wasm") {
if (!replayWasmMotion) throw new Error("wasm_motion_replay_unavailable");
const queueTick = replayWasmMotion.read().state.tick;
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
const cycles = Number.isSafeInteger(event.payload?.cycles) && event.payload.cycles >= 0 ? event.payload.cycles : 1;
for (let cycle = 0; cycle < cycles; cycle += 1) {
const replayTick = queueTick + cycle + 1;
const steps = replayProgram.tick(1);
for (const step of steps) {
if (!step.motion) continue;
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) {
const enqueue = replayWasmMotion.enqueue(step.motion);
if (enqueue.status === "error") throw new Error(enqueue.code);
}
}
const stepped = replayWasmMotion.step();
if (stepped.status === "error" && stepped.code !== "wasm_motion_step_-605") throw new Error(stepped.code);
if (stepped.status === "verified") {
const projected = replayMotion.applyWasmPosition(stepped.state.position, stepped.state.velocity, replayTick);
if (projected.status === "error") throw new Error(projected.code);
if (replayHomeLimit?.read().state.homed) {
const checked = replayHomeLimit.check([stepped.state.position.x, stepped.state.position.y, stepped.state.position.z]);
if (checked.status === "error") throw new Error(checked.code);
}
}
}
} else {
if (Number.isSafeInteger(state.tick) && replayAuthoritative.state.tick < state.tick) replayAuthoritative.state.tick = state.tick;
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
for (let replayTick = replayAuthoritative.state.tick + 1; replayTick <= event.tick; replayTick += 1) {
replayProgram.tick(1);
const synced = replayAuthoritative.syncProgram(replayProgram.snapshot());
if (synced.status === "blocked") throw new Error(synced.code);
const stepped = replayAuthoritative.step(replayTick);
if (stepped.status === "error") throw new Error(stepped.code);
}
}
} else if (event.kind === "SIM_STEP") {
if (replayMotionAuthority === "wasm") {
const stepped = replayWasmMotion?.step();
if (stepped?.status === "error" && stepped.code !== "wasm_motion_step_-605") throw new Error(stepped.code);
} else {
const stepped = replayAuthoritative.step(event.tick);
if (stepped.status === "error") throw new Error(stepped.code);
}
}
if (event.kind === "PLC_CONFIG") replayPlc.configure(event.payload);
if (event.kind === "PLC_SET") replayPlc.setRegister(event.payload?.register, event.payload?.value);
if (event.kind === "PLC_TRIGGER") replayPlc.trigger();
if (event.kind === "SIM_STEP") for (let replayTick = replayPlc.state.tick + 1; replayTick <= event.tick; replayTick += 1) replayPlc.scan(replayTick);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "configure") replayHal.configure(event.payload);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "thread") replayHal.registerThread(event.payload?.name, event.payload?.period_ticks);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "component") replayHal.registerComponent(event.payload?.instance, event.payload?.type, event.payload?.thread);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "connect") replayHal.connect(event.payload?.source, event.payload?.target);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "failure") replayHal.setFailure(event.payload?.instance, event.payload?.failed === true);
if (event.kind === "HAL_SET" && event.payload?.target === "pin") replayHal.setPin(event.payload?.name, event.payload?.value, event.payload?.type);
if (event.kind === "HAL_SET" && event.payload?.target === "param") replayHal.setParam(event.payload?.name, event.payload?.value, event.payload?.type);
if (event.kind === "SIM_STEP") replayHal.step(event.tick);
if (event.kind === "ENCODER_SET") replayDeviceIo.encoderSet(event.payload?.channel, event.payload);
if (event.kind === "COOLANT_COMMAND") replayDeviceIo.coolantCommand(event.payload);
if (event.kind === "COOLANT_FAULT") replayDeviceIo.coolantFault(event.payload?.fault === true);
if (event.kind === "DOOR_SET") replayDeviceIo.doorSet(event.payload?.open === true);
if (event.kind === "INTERLOCK_SET") replayDeviceIo.interlockSet(event.payload?.input_ok === true);
if (event.kind === "INTERLOCK_RESET") replayDeviceIo.interlockReset();
if (event.kind === "DEVICE_ESTOP") replayDeviceIo.estop(event.payload?.active === true);
if (event.kind === "DEVICE_ACK_ISSUE") replayDeviceIo.issueAck(event.operation_id ?? event.payload?.operation_id, event.payload?.timeout_ticks ?? 10);
if (event.kind === "DEVICE_ACK") replayDeviceIo.ack(event.payload?.operation_id);
if (event.kind === "DEVICE_ACK_ABORT") replayDeviceIo.abortAck(event.payload?.operation_id);
if (event.kind === "SIM_STEP") replayDeviceIo.step(event.tick);
if (event.kind === "CONFIGURATION_BATCH") replayConfigurationBatch.load(event.payload?.entries ?? [], { context_id: this.contextId });
if (event.kind === "IO_COMMAND") replayIoAck.issue(event.payload?.operation_id ?? event.payload?.command_id, event.payload?.kind ?? "IO", event.tick, event.payload?.timeout_ticks ?? 10);
if (event.kind === "IO_COMMAND") replayDeviceIo.issueAck(event.payload?.operation_id ?? event.payload?.command_id, event.payload?.timeout_ticks ?? 10);
if (event.kind === "IO_ACK") replayIoAck.ack(event.payload?.operation_id, event.tick);
if (event.kind === "IO_ACK") replayDeviceIo.ack(event.payload?.operation_id);
if (event.kind === "IO_TIMEOUT") replayIoAck.step(event.tick);
if (event.kind === "SIM_STEP") replayIoAck.step(event.tick);
if (event.kind === "IO_TIMEOUT") replayDeviceIo.ackStep(event.tick);
if (event.kind === "REMAP_REGISTER") replayCallbacks.registerRemap(event.payload?.name, event.payload);
if (event.kind === "VCP_REGISTER") replayCallbacks.registerVcp(event.payload?.name, event.payload?.pins ?? []);
if (event.kind === "FILTER_EXEC") replayCallbacks.filter(event.payload?.name, event.payload?.source ?? "", event.payload?.args ?? []);
if (event.kind === "REMAP_CALL") replayCallbacks.invoke("remap", event.payload?.name, event.payload?.phase ?? "prolog", event.payload?.args ?? []);
if (event.kind === "VCP_UPDATE") replayCallbacks.updateVcp(event.payload?.name, event.payload?.pin, event.payload?.value);
if (event.kind === "PYTHON_IMPORT") replayPython.importModule(event.payload?.module);
if (event.kind === "PYTHON_CALL") replayPython.invoke(event.payload?.module, event.payload?.symbol, event.payload?.args ?? []);
if (event.kind === "PYTHON_CAPABILITY") replayPython.checkCapability(event.payload?.capability);
if (event.kind === "SIM_STEP") replayCallbacks.setTick(event.tick);
if (event.kind === "SIM_STEP") replayPython.setTick(event.tick);
const task = { ...(state.task ?? this.task.export()), seq: event.seq, simulation_tick: event.tick };
delete task.state_hash;
if (event.kind === "ESTOP" && event.payload?.active === true) {
task.state = "ESTOP";
} else if (event.kind === "IO_RECOVER") {
task.state = "READY";
} else if (event.kind === "PROGRAM_PAUSE") {
task.state = "PAUSED";
task.program_status = "paused";
} else if (event.kind === "PROGRAM_RESUME" || event.kind === "PROGRAM_RUN") {
task.state = "RUNNING";
task.program_status = "running";
} else if (event.kind === "PROGRAM_ABORT" || event.kind === "ABORT") {
task.state = "STOPPED";
task.program_status = "aborted";
}
if (replayProgram.loaded) {
const programStatus = replayProgram.status === "completed" ? "completed" : replayProgram.status === "running" ? "running" : replayProgram.status === "paused" ? "paused" : replayProgram.status === "aborted" ? "aborted" : "loaded";
task.current_line = replayProgram.currentLine;
task.program_status = programStatus;
if (programStatus === "completed" && task.state === "RUNNING" && (replayMotionAuthority !== "wasm" || replayWasmMotion?.read().state.queue_depth === 0)) task.state = "STOPPED";
}
const schedulerUnsigned = {
schema: "sim_scheduler.v1",
budget: state.scheduler?.budget ?? 1000000,
phase_trace: [...SCHEDULER_PHASES],
tick: event.tick,
};
const scheduler = {
...schedulerUnsigned,
last_cycles: event.kind === "SIM_STEP" && Number.isSafeInteger(event.payload?.cycles) ? event.payload.cycles : (state.scheduler?.last_cycles ?? 0),
state_hash: fnv1a64(schedulerUnsigned),
};
return { ...state, tick: event.tick, motion_authority: replayMotionAuthority, task_authority: replayMotionAuthority === "wasm" ? "wasm_task_canonical" : "js_compat", task: { ...task, state_hash: fnv1a64(task) }, scheduler, devices: replayDevices.export(), motion: replayMotion.export(), ...(replayWasmMotion ? { wasm_motion: replayWasmMotion.exportState() } : {}), ...(replayHomeLimit ? { wasm_home_limit: replayHomeLimit.exportState() } : {}), authoritative: replayAuthoritative.export(), plc: replayPlc.export(), hal_runtime: replayHal.export(), device_io: replayDeviceIo.export(), configuration_batch: replayConfigurationBatch.export(), io_ack: replayIoAck.export(), callbacks: replayCallbacks.export(), python: replayPython.exportState(), ...(replayProgram.loaded ? { program: replayProgram.exportState() } : {}) };
},
});
replayWasmMotion?.destroy();
replayHomeLimit?.destroy();
if (result.status === "recovered" || result.status === "aborted") {
this.tick = result.simulation_tick;
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();
if (result.state?.authoritative) this.authoritative.restore(result.state.authoritative);
this.devices = new VirtualDevices({ state: result.state?.devices ?? checkpoint?.state?.devices });
this.motion = new MotionState();
if (result.state?.motion) this.motion.restore(result.state.motion);
if (this.motionAuthority !== "wasm") this.authoritative.applyMotion(this.motion);
if (this.motionAuthority === "wasm") {
const wasmMotion = this.wasmMotion.read();
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);
this.hal = new HalRuntime();
if (result.state?.hal_runtime) this.hal.restore(result.state.hal_runtime);
this.deviceIo = new DeviceIoRuntime();
if (result.state?.device_io) this.deviceIo.restore(result.state.device_io);
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId });
if (result.state?.configuration_batch) this.configurationBatch.restore(result.state.configuration_batch);
this.ioAck = new IoAckTracker();
if (result.state?.io_ack) this.ioAck.restore(result.state.io_ack);
this.callbacks = new PythonCallbacks();
if (result.state?.callbacks) this.callbacks.restore(result.state.callbacks);
this.configurationState = result.state?.configuration ?? checkpoint?.state?.configuration ?? null;
this.halState = result.state?.hal ?? checkpoint?.state?.hal ?? null;
this.previewState = result.state?.preview ?? checkpoint?.state?.preview ?? null;
this.python = new PythonBridge();
if (result.state?.python) this.python.restoreState(result.state.python);
this.program = new ProgramRunner(this.core);
if (result.state?.program) this.program.restoreState(result.state.program);
this.timeline = new ReplayTimeline(checkpoint.timeline);
if (events?.schema === "replay-timeline.v1") {
for (const event of events.events.events.slice(this.timeline.events.cursor)) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash);
} else if (Array.isArray(events)) {
for (const event of events) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash);
}
this.operations = new Map(result.operations.map((operation) => [operation.operation_id, operation]));
this.ledger = new OperationLedger();
for (const operation of result.operations) {
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 { /* legacy checkpoint entries remain in operations */ }
}
}
return result;
}
validateEnvelope(input) {
if (!input || typeof input !== "object") return ["protocol_argument", "invalid_envelope"];
if (input.schema_version !== PROTOCOL_SCHEMA) return ["protocol_schema", "unsupported_schema"];
if (input.context_id !== this.contextId) return ["protocol_context", "context_mismatch"];
if (!text(input.kind) || !Number.isSafeInteger(input.seq) || input.seq <= 0) return ["protocol_argument", "invalid_sequence_or_kind"];
if (this.sessionId && input.session_id !== this.sessionId) return ["protocol_session", "session_mismatch"];
if (!this.sessionId && text(input.session_id)) {
this.sessionId = input.session_id;
this.task.bindSession(this.sessionId);
}
return null;
}
accept(input) {
const invalid = this.validateEnvelope(input);
if (invalid) return this.error(input ?? {}, invalid[0], invalid[1]);
const result = call(this.core, "sim_protocol_accept", input.schema_version, input.seq, input.kind, input.operation_id ?? "", input.simulation_tick ?? this.tick);
if (result !== 0) return this.coreError(input, result, "sim_protocol_accept");
let ledgerPreparation;
try {
ledgerPreparation = this.ledger.prepare(input);
if (ledgerPreparation.replay && ledgerPreparation.entry?.response) {
const replayTick = Number.isSafeInteger(input.simulation_tick) && input.simulation_tick >= this.tick ? input.simulation_tick : this.tick;
this.timeline.appendEvent(input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: ledgerPreparation.entry.seq });
const replayEventResult = appendCoreEvent(this.core, input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: ledgerPreparation.entry.seq });
if (replayEventResult !== 0) return this.error(input, "event_log", "core_event_append_failed");
this.tick = replayTick;
try { this.task.setCommand(input.seq, replayTick); } catch (error) { return this.error(input, "task_state", String(error?.message ?? error)); }
return { ...ledgerPreparation.entry.response, replayed: true, seq: input.seq, ledger: this.ledger.reconcile(input.operation_id) };
}
if (ledgerPreparation.entry) this.ledger.execute(input.operation_id);
} catch (error) {
const ledgerCode = String(error?.message ?? error);
try {
this.task.setCommand(input.seq, input.simulation_tick ?? this.tick);
this.tick = this.task.state.simulation_tick;
} catch { /* protocol has already accepted the command; retain ledger error */ }
try {
this.timeline.appendEvent(input.seq, this.tick, "COMMAND_REJECTED", { code: ledgerCode, operation_id: input.operation_id ?? null });
appendCoreEvent(this.core, input.seq, this.tick, "COMMAND_REJECTED", { code: ledgerCode, operation_id: input.operation_id ?? null });
} catch { /* preserve the original ledger diagnostic */ }
return this.error(input, ledgerCode.startsWith("ledger_") ? ledgerCode : "ledger", ledgerCode);
}
try {
this.task.setCommand(input.seq, input.simulation_tick ?? this.tick);
this.tick = this.task.state.simulation_tick;
} catch (error) {
return this.error(input, "task_state", String(error?.message ?? error));
}
const existingOperation = this.operationState(input);
if (existingOperation?.response) {
if (existingOperation.kind !== input.kind) return this.error(input, "recovery_operation_conflict", "operation_id_conflict");
try {
const replayTick = Number.isSafeInteger(input.simulation_tick) && input.simulation_tick >= this.tick ? input.simulation_tick : this.tick;
this.timeline.appendEvent(input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: existingOperation.seq });
const replayEventResult = appendCoreEvent(this.core, input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: existingOperation.seq });
if (replayEventResult !== 0) return this.error(input, "event_log", "core_event_append_failed");
this.tick = replayTick;
return { ...existingOperation.response, replayed: true, seq: input.seq };
} catch (error) {
return this.error(input, "event_log", String(error?.message ?? error));
}
}
if (text(input.operation_id) && OPERATION_KINDS.has(input.kind)) {
this.operations.set(input.operation_id, { operation_id: input.operation_id, status: "executing", seq: input.seq, kind: input.kind, result_hash: null });
}
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.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 {
this.timeline.appendEvent(input.seq, this.tick, "COMMAND_REJECTED", rejectedPayload);
const rejectedEvent = appendCoreEvent(this.core, input.seq, this.tick, "COMMAND_REJECTED", rejectedPayload);
if (rejectedEvent !== 0) return this.error(input, "event_log", "core_event_append_failed");
} catch (error) {
return this.error(input, "event_log", String(error?.message ?? error));
}
return this.envelope("ERROR", input, {
status: "blocked",
code: "axis_gate",
message_key: gate.reason,
gate_reason: gate.reason,
reason: gate.reason,
error_code: "axis_gate",
source: "linuxcnc-wasm-worker",
retryable: false,
operation_id: input.operation_id ?? null,
});
}
if (Number.isSafeInteger(input.simulation_tick) && input.simulation_tick >= this.tick) this.tick = input.simulation_tick;
const eventTick = input.kind === "SIM_STEP" && Number.isSafeInteger(input.payload?.cycles)
? this.tick + input.payload.cycles : this.tick;
try {
this.timeline.appendEvent(input.seq, eventTick, input.kind, input.payload ?? {});
} catch (error) {
if (text(input.operation_id)) this.operations.delete(input.operation_id);
return this.error(input, "event_log", String(error?.message ?? error));
}
const coreEventResult = appendCoreEvent(this.core, input.seq, eventTick, input.kind, input.payload ?? {});
if (coreEventResult !== 0) {
if (text(input.operation_id)) this.operations.delete(input.operation_id);
return this.error(input, "event_log", "core_event_append_failed");
}
const finish = (response, status = "verified") => {
if (text(input.operation_id) && OPERATION_KINDS.has(input.kind)) {
const operation = this.operations.get(input.operation_id);
if (operation) {
operation.status = status;
operation.result_hash = fnv1a64(response.payload ?? {});
operation.response = response;
}
try { this.ledger.verify(input.operation_id, response); this.ledger.commit(input.operation_id, input.seq); } catch (error) {
const ledgerCode = String(error?.message ?? error);
return this.error(input, ledgerCode.startsWith("ledger_") ? ledgerCode : "ledger", ledgerCode);
}
}
return response;
};
const deviceResponse = (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) {
// LinuxCNC emcTaskSetState(ESTOP) calls emcTaskAbort: clear the
// interpreter command and trajectory, then leave the taskfile loaded
// for a later AUTO run after ESTOP_RESET and POWER ON.
this.xyzacProgram.abort();
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
const resetProgram = this.resetXyzacProgramToLoaded();
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
this.task.setProgram("loaded", 0);
}
try {
if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904);
else if (kind === "ESTOP" && payload?.active === false && result.status === "verified" && this.xyzacProgram && this.task.state.state === "ESTOP") this.task.transition("STOPPED");
else if (kind === "IO_RECOVER" && result.status === "verified") this.task.recover();
else if (kind === "TOOL_FAULT" && payload?.fault === true) this.task.fault(-905);
} catch (error) {
return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed");
}
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.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");
};
const deviceIoResponse = (kind, payload = {}) => {
let result;
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);
else if (kind === "INTERLOCK_SET") result = this.deviceIo.interlockSet(payload.input_ok === true);
else if (kind === "INTERLOCK_RESET") result = this.deviceIo.interlockReset();
else if (kind === "DEVICE_ESTOP") result = this.deviceIo.estop(payload.active === true);
else if (kind === "DEVICE_ACK_ISSUE") result = this.deviceIo.issueAck(input.operation_id ?? payload.operation_id, payload.timeout_ticks ?? 10);
else if (kind === "DEVICE_ACK_ABORT") result = this.deviceIo.abortAck(payload.operation_id);
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.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) {
case "HELLO":
return finish(this.envelope("CAPABILITIES", input, capabilities(this.contextId)), "verified");
case "SESSION_ATTACH":
if (!this.created) {
const createResult = call(this.core, "sim_create", 0, 0);
if (createResult !== 0) return finish(this.coreError(input, createResult, "sim_create"), "failed");
this.created = true;
}
try { this.task.transition("READY"); } catch (error) { return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed"); }
return finish(this.envelope("SESSION_READY", input, { state_hash: this.task.hash(), task: this.task.export(), subscriptions: [] }), "verified");
case "SET_MODE": {
if (this.xyzacProgram) {
const gateResult = this.xyzacCommandGate(input);
if (!gateResult.enabled) return finish(this.envelope("ERROR", input, { status: "blocked", code: "axis_gate", gate_reason: gateResult.reason, message_key: gateResult.reason, operation_id: input.operation_id ?? null }), "failed");
}
const mode = String(input.payload?.mode ?? "").toUpperCase();
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
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 (this.xyzacProgram) {
const result = this.xyzacIo.command("POWER", input.payload ?? {});
let recoveryCheckpoint = null;
let recoveryPersistence = { status: "skipped", reason: "power_on" };
if (result.status === "verified") {
if (input.payload?.active === false) {
/* LinuxCNC emcTaskSetState(OFF) aborts motion, clears the
interpreter command and leaves the loaded taskfile intact. */
this.xyzacProgram.abort();
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
const resetProgram = this.resetXyzacProgramToLoaded();
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
this.task.setProgram("loaded", 0);
}
try {
if (input.payload?.active === true) this.task.recover();
else if (["READY", "RUNNING", "PAUSED", "STOPPED"].includes(this.task.state.state)) {
if (["READY", "RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
}
} catch { /* the WASM IO state remains authoritative */ }
if (input.payload?.active === false) {
try {
recoveryCheckpoint = this.createPowerOffCheckpoint();
recoveryPersistence = { status: "checkpoint_ready", persistence: "browser_client_storage" };
} catch (error) {
recoveryPersistence = { status: "blocked", code: "runtime_checkpoint_create_failed", reason: String(error?.message ?? error) };
}
}
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: input.payload?.active === true, task: this.task.export(), device_io: this.xyzacIo.export(), ...(recoveryCheckpoint ? { recovery_checkpoint: recoveryCheckpoint } : {}), recovery_persistence: recoveryPersistence, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
if (input.payload?.active !== true) return finish(this.error(input, "power_state", "power_active_required"), "failed");
const result = this.xyzacIo ? this.xyzacIo.command("POWER", 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 (this.xyzacProgram) {
const result = this.xyzacIo.command("RESET");
if (result.status === "verified" && ["FAULT", "STOPPED"].includes(this.task.state.state)) {
try { this.task.recover(); } catch { /* retain the WASM IO result */ }
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, task: this.task.export(), device_io: this.xyzacIo.export(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
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.motionProjection(), operation_id: input.operation_id ?? null }));
}
case "LOAD_WORKSPACE": {
const rootPath = input.payload?.root_path;
if (!text(rootPath) || !rootPath.startsWith("/") || rootPath.includes("..")) return finish(this.error(input, "workspace_path", "invalid_workspace_path"), "failed");
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({
workspace: startupWorkspace,
entrypoint: configuration.entrypoint,
variables: configuration.variables ?? {},
maxDepth: configuration.maxDepth ?? 32,
core: this.core,
coreCreated: this.created,
task: this.task,
halManifest: configuration.halManifest ?? configuration.hal_manifest ?? configuration.manifest ?? [],
});
const startupReport = startup.run();
if (startupReport.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "configuration_startup",
message_key: "configuration.startup_blocked",
retryable: false,
startup: startupReport,
}), "failed");
}
this.configurationState = startupReport;
this.created = true;
startupLoaded = true;
} else {
const preflight = preflightConfiguration(input.payload.configuration);
if (preflight.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "configuration_preflight",
message_key: "configuration.preflight_blocked",
retryable: false,
preflight,
}), "failed");
}
this.configurationState = {
entrypoint: input.payload.configuration.entrypoint ?? null,
status: preflight.status,
resource_count: preflight.resource_count,
state_hash: fnv1a64(preflight),
};
}
}
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 });
const initialPower = this.xyzacIo.command("POWER", { active: false });
if (initialPower.status !== "verified") throw new Error(initialPower.code ?? "wasm_power_off_init");
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, {
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);
if (preflight.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "hal_preflight",
message_key: "hal.preflight_blocked",
retryable: false,
preflight,
}), "failed");
}
this.halState = {
status: preflight.status,
record_count: preflight.record_count,
state_hash: fnv1a64(preflight),
};
}
const configuredKinematics = input.payload?.kinematics ?? configuration?.kinematics?.type ?? null;
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");
}
const loadResult = startupLoaded ? 0 : call(this.core, "sim_load_workspace", rootPath);
let powerOffRecovery = null;
if (loadResult === 0 && input.payload?.recovery_checkpoint && this.xyzacProgram) {
const restored = this.restorePersistedPowerOff(input.payload.recovery_checkpoint, input.seq);
if (restored.status !== "verified") return finish(this.envelope("ERROR", input, {
status: "blocked",
code: restored.code ?? "runtime_checkpoint_restore_failed",
message_key: restored.reason ?? restored.code ?? "runtime_checkpoint_restore_failed",
reason: restored.reason ?? restored.code ?? "runtime_checkpoint_restore_failed",
recovery: restored,
}), "failed");
powerOffRecovery = restored;
}
return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, {
status: "verified",
command: input.kind,
motion_authority: this.motionAuthority,
state_hash: this.task.hash(),
task: this.task.export(),
...(powerOffRecovery ? { power_off_recovery: powerOffRecovery } : {}),
...(this.configurationState?.schema === "configuration-startup-runtime.v1" ? { configuration: this.configurationState } : {}),
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
}
case "SIM_STEP": {
const cycles = input.payload?.cycles;
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;
// 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.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) {
if (!step.motion) continue;
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) {
const enqueue = this.wasmMotion.enqueue(step.motion);
if (enqueue.status === "error") throw new Error(enqueue.code);
}
}
},
tp: (nextTick) => {
if (this.motionAuthority !== "wasm" && this.program.loaded) {
const sync = this.authoritative.syncProgram(this.program.snapshot());
if (sync.status === "blocked") throw new Error(sync.code);
}
if (this.motionAuthority !== "wasm") {
const planner = this.authoritative.step(nextTick);
if (planner.status === "error") throw new Error(planner.code);
}
},
kinematics: () => {
if (this.motionAuthority !== "wasm" && !["trivkins", "corexy", "tripod", "lineardelta", "rotarydelta"].includes(this.authoritative.state.kinematics.type)) throw new Error("authoritative_kinematics_unsupported");
},
motion: (nextTick) => {
const stepResult = call(this.core, "sim_step", 1);
if (stepResult !== 0) throw new Error(`core_sim_step_${stepResult}`);
this.tick = nextTick;
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);
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) {
// 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) {
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.xyzacIo ? this.xyzacIo.halCommand({ action: "step", ticks: 1 }) : this.hal.step(nextTick);
if (halState.status === "error") throw new Error(halState.code);
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);
if (ackState.status === "error") throw new Error(ackState.code);
const callbackClock = this.callbacks.setTick(nextTick);
if (callbackClock.status === "blocked") throw new Error(callbackClock.code);
this.python.setTick(nextTick);
const protocolTick = call(this.core, "sim_protocol_set_tick", nextTick);
if (protocolTick !== 0) throw new Error(`protocol_tick_${protocolTick}`);
},
});
} catch (error) {
return finish(this.error(input, "scheduler", String(error?.message ?? error)), "failed");
}
const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : null;
const execution_trace = program_steps
.map((step) => ({
line: step.line ?? step.program?.execution_line ?? null,
text: step.program?.execution_line_text ?? "",
program_status: step.program_status ?? step.program?.program_status ?? null,
}))
.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);
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() };
if (program) snapshotState.program = program;
const snapshot = 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,
...(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();
this.wasmMotion.reset();
this.wasmMotion.setPaused(false);
this.motion.applyWasmPosition(this.wasmMotion.read().state.position, 0, this.tick);
this.task.setProgram("loaded", 0);
const preview = this.program.preview();
result.preview = preview.preview ?? { status: "error", code: preview.code ?? "program_preview_failed" };
this.previewState = result.preview;
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
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");
if (this.task.state.state === "STOPPED") this.task.recover();
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(false);
this.task.transition("RUNNING");
this.task.setProgram("running", this.program.currentLine);
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
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") {
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(true);
this.task.transition("PAUSED");
} else if (action === "resume") {
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(false);
this.task.transition("RUNNING");
} else {
if (this.motionAuthority === "wasm") this.wasmMotion.abort();
else this.authoritative.abort();
this.task.transition("STOPPED");
this.task.setProgram("aborted", this.program.currentLine);
}
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
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");
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
if (this.task.state.state === "STOPPED") this.task.recover();
this.task.setMode("AUTO");
this.authoritative.reset();
this.wasmMotion.reset();
this.wasmMotion.setPaused(false);
this.motion.applyWasmPosition(this.wasmMotion.read().state.position, 0, this.tick);
this.task.setProgram("loaded", 0);
}
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");
}
case "PROGRAM_STEP": {
if (this.xyzacProgram) {
const current = this.xyzacProgram.snapshot();
if (current.status === 1 && this.task.state.mode !== "AUTO") this.task.setMode("AUTO");
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");
}
const result = this.program.step();
if (result.status === "verified") {
if (this.motionAuthority === "wasm" && result.motion) {
const linearNoop = result.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => result.motion.start[axis] === result.motion.end[axis]);
if (!linearNoop) {
const enqueue = this.wasmMotion.enqueue(result.motion);
if (enqueue.status === "error") return finish(this.error(input, "motion_queue", enqueue.code), "failed");
}
this.wasmMotion.setPaused(true);
}
this.task.setProgram(result.program_status === "completed" ? "completed" : "paused", result.line);
if (result.program_status === "completed") {
const pending = this.motionAuthority === "wasm" ? this.wasmMotion.read().state.queue_depth > 0 : this.authoritative.state.planner.queue_depth > 0;
if (this.task.state.state === "RUNNING") this.task.transition(pending ? "PAUSED" : "STOPPED");
} else if (this.task.state.state === "RUNNING") this.task.transition("PAUSED");
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
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, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
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 block = input.payload?.block ?? input.payload?.command;
if (this.xyzacProgram) {
this.task.setMode("MDI");
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,
task: this.task.export(),
command: input.kind,
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");
this.task.setMode("MDI");
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.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");
try {
this.wasmMotion.setFeedOverride(feed);
} catch (error) {
return finish(this.error(input, "motion_override", String(error?.message ?? error)), "failed");
}
const motion = this.wasmMotionProjection();
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, feed_override: feed, overrides: { feed, spindle: [this.wasmMotionProjection().spindle_override] }, state: motion, motion, wasm_motion: this.wasmMotion.exportState(), 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);
else if (input.kind === "JOG_STOP") result = this.motion.stopJog();
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.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":
case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE":
case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT":
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");
}
case "PLC_CONFIG": case "PLC_SET": case "PLC_TRIGGER": {
const result = input.kind === "PLC_CONFIG" ? this.plc.configure(input.payload) : input.kind === "PLC_SET" ? this.plc.setRegister(input.payload?.register, input.payload?.value) : this.plc.trigger();
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), plc: this.plc.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "CONFIGURATION_BATCH": {
const result = this.configurationBatch.load(input.payload?.entries, { context_id: this.contextId });
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, configuration_batch: this.configurationBatch.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "HAL_COMMAND": case "HAL_SET": {
const payload = input.payload ?? {};
let result;
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);
} else if (payload.action === "thread") {
result = this.hal.registerThread(payload.name, payload.period_ticks);
} else if (payload.action === "component") {
result = this.hal.registerComponent(payload.instance ?? payload.name, payload.type, payload.thread ?? "servo-thread");
} else if (payload.action === "connect") {
result = this.hal.connect(payload.source, payload.target);
} else if (payload.action === "failure") {
result = this.hal.setFailure(payload.instance, payload.failed === true);
} else if (payload.action === "step") {
result = this.hal.step(payload.tick ?? this.tick);
} 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.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);
case "IO_COMMAND": case "IO_ACK": case "IO_TIMEOUT": {
let result;
if (input.kind === "IO_COMMAND") {
result = this.ioAck.issue(input.operation_id, input.payload?.kind ?? "IO", this.tick, input.payload?.timeout_ticks ?? 10);
if (result.status === "verified") this.deviceIo.issueAck(input.operation_id, input.payload?.timeout_ticks ?? 10);
} else if (input.kind === "IO_ACK") {
result = this.ioAck.ack(input.payload?.operation_id, this.tick);
this.deviceIo.ack(input.payload?.operation_id);
} else {
const targetTick = this.tick + (input.payload?.ticks ?? 0);
result = this.ioAck.step(targetTick);
this.deviceIo.ackStep(targetTick);
}
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"
? this.callbacks.invoke("remap", input.payload?.name, input.payload?.phase ?? "prolog", input.payload?.args ?? [])
: input.kind === "VCP_REGISTER"
? this.callbacks.registerVcp(input.payload?.name, input.payload?.pins ?? [])
: input.kind === "VCP_UPDATE"
? this.callbacks.updateVcp(input.payload?.name, input.payload?.pin, input.payload?.value)
: this.callbacks.filter(input.payload?.name, input.payload?.source ?? "", input.payload?.args ?? []);
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");
return deviceResult;
}
case "PYTHON_IMPORT": {
const result = this.python.importModule(input.payload?.module);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "PYTHON_CALL": {
const result = this.python.invoke(input.payload?.module, input.payload?.symbol, input.payload?.args ?? []);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "PYTHON_CAPABILITY": {
const result = this.python.checkCapability(input.payload?.capability);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "SNAPSHOT_REQUEST":
return finish(this.envelope("STATE_SNAPSHOT", input, { snapshot_schema: 1, tick: this.tick, task: this.task.export(), state_hash: this.task.hash(), snapshot: this.timeline.snapshots.latest() }));
case "CHECKPOINT": {
try {
const operation = text(input.operation_id) ? this.operations.get(input.operation_id) : null;
if (operation) operation.status = "verified";
const checkpoint = this.createCheckpoint();
return finish(this.envelope("CHECKPOINT_READY", input, { status: "verified", checkpoint, state_hash: checkpoint.checkpoint_hash }));
} catch (error) {
return finish(this.error(input, "recovery_checkpoint", String(error?.message ?? error)), "failed");
}
}
case "RECOVER": {
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;
if (operation && operation.status !== "verified") operation.status = "aborted";
let ledgerResult = null;
if (text(target)) {
try { ledgerResult = this.ledger.abort(target); } catch (error) { ledgerResult = { status: "missing", error: String(error?.message ?? error) }; }
}
if (this.task.state.state !== "DESTROYED") {
try { this.task.transition("STOPPED"); } catch { /* target abort can be bookkeeping-only */ }
}
return finish(this.envelope("COMMAND_RESULT", input, { status: ledgerResult?.status === "too_late" ? "too_late" : operation ? "aborted" : "verified", operation_id: target ?? null, ledger: ledgerResult }));
}
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");
this.created = false;
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", destroyed: true, replay: this.timeline.export() }));
default:
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind }));
}
}
}
export { CONTEXT_ID, PROTOCOL_SCHEMA };