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 { axisSourceUi } from "./axis-source-ui.mjs"; import { WasmMotionQueue } from "./wasm-motion-queue.mjs"; import { WasmLinuxcncTp } from "./wasm-linuxcnc-tp.mjs"; import { WasmHomeLimit } from "./wasm-home-limit.mjs"; import { WasmNativeHoming } from "./wasm-native-homing.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"; import { machineDefinition } from "../workspace/machine-definitions.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", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_CONSTANT", "SPINDLE_BRAKE", "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", "TOUCH_OFF", "TOOL_TOUCH_OFF", "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", SET_JOG_SCALE: "jogStart", SET_MAX_VELOCITY: "override", SET_SPINDLE_OVERRIDE: "override", HOME: "home", SET_OVERRIDE: "override", SPINDLE_COMMAND: "spindle", SPINDLE_CONSTANT: "spindle", SPINDLE_BRAKE: "spindle", COOLANT_COMMAND: "coolant", TOOL_PREPARE: "tool", TOOL_CHANGE: "tool", PROBE_CONFIG: "probe", 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, SPINDLE: 17, COOLANT: 18, TOOL: 19, PROBE: 20, TOUCH_OFF: 21, TOOL_TOUCH_OFF: 22, }); 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(",")}}`; } // Control-flow contract: docs/linuxcnc-axis-web-control-panel-source-logic.md // maps each browser operation to its LinuxCNC AXIS source symbol and gate. export class SimSession { constructor(core, { contextId = CONTEXT_ID, sessionId = "", planner = "queue" } = {}) { this.core = core; this.contextId = contextId; this.sessionId = sessionId; this.planner = planner; this.tick = 0; this.created = false; this.timeline = new ReplayTimeline(); 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 = planner === "linuxcnc-tp" ? new WasmLinuxcncTp(core) : new WasmMotionQueue(core); this.wasmHomeLimit = new WasmHomeLimit(core); this.xyzacKinematics = null; this.xyzacIo = null; this.xyzacDeviceIo = null; this.xyzacProgram = null; this.machineDefinition = null; this.homingHistory = []; this.xyzacProgramKinematicsMode = XYZAC_MODES.identity; this.xyzacRapidVelocity = 35; this.xyzacMotionCycleTime = 0.01; this.xyzacTaskCycleTicks = 1; this.xyzacAxisVelocityLimits = {}; this.xyzacAxisAccelerationLimits = {}; this.pendingProgramControl = 0; this.pendingTaskSync = 0; this.pendingXyzacMotion = null; this.xyzacStepTargetId = 0; this.pendingServoSync = 0; this.xyzacExecutionWaitNextMotion = false; this.xyzacExecutionWaitSamples = 0; this.xyzacLastEnqueuedKinematicsMode = 0; this.xyzacDeferredPhysicalSwitchMode = null; this.xyzacDeferredBlockedActiveId = 0; this.xyzacDeferredControlMotionType = 0; this.xyzacQueuedLookaheadTargetId = 0; this.xyzacAllowQueuedLookahead = false; this.xyzacLastServoActiveId = 0; this.xyzacLastQueuedMotion = null; this.xyzacDeferredPhysicalPose = null; this.xyzacDeferredPhysicalPoseApplied = false; this.xyzacAppendDeferredOnActive = false; this.xyzacDeferredCompletionActiveId = 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.livePathEpoch = 0; this.workspaceFiles = {}; this.visualModelSourceHash = null; 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 published by the latest program transaction. const xyzacProgramActive = Boolean(this.xyzacProgram); const lightweightXyzacStep = input.kind === "SIM_STEP" && xyzacProgramActive; const traceOnlyStep = lightweightXyzacStep && input.payload?.trace_only === true; const currentMotion = traceOnlyStep && statePayload.wasm_motion ? statePayload.wasm_motion : this.wasmMotion.read(); const completedMotion = program.status === 4 && currentMotion.state?.queue_depth === 0; // Compact payloads are only valid for successful motion samples. Error // responses must retain their code/reason instead of being rewritten into // a delta with undefined fields. const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion && !statePayload.homing_sample && !this.wasmHomeLimit.active; // The frontend retains the canonical preview from PROGRAM_LOAD. Repeating // thousands of immutable segments on unrelated control responses makes a // loaded program delay abort/power/home commands behind structured-clone // work. Explicit preview transactions still publish the full replacement. const publishXyzacToolpath = ["PROGRAM_LOAD", "PROGRAM_RESTART", "RECOVER", "SNAPSHOT_REQUEST", "VISUAL_CLEAR"].includes(input.kind); const panelSnapshot = motionDelta || traceOnlyStep ? null : this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: xyzacProgramActive ? publishXyzacToolpath : !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, feed: program.feed, speed: program.speed, units: program.units, distance_mode: program.distance_mode, motion_control: program.motion_control, motion_control_mode: program.motion_control_mode, path_tolerance: program.path_tolerance, } : 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, ...(motionDelta ? { source_status: { task: { mode: task.mode, state: axisState({ task, program, io: this.xyzacIo.export() }).source_task_state, interpState: axisState({ task, program, io: this.xyzacIo.export() }).source_interp_state }, traj: { mode: this.wasmHomeLimit.mode, motion_type: currentMotion.state.motion_type }, homing_active: this.wasmHomeLimit.active, joint: this.wasmHomeLimit.read().state.joint, homing_history: this.homingHistory, } } : {}), ...(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() } : {}), ...(traceOnlyStep ? {} : { axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export() }) }), }, }; } wasmMotionProjection(program = null) { 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 }; if (this.xyzacProgram && !this.wasmHomeLimit.active) this.wasmHomeLimit.sync(wasmMotion.state.joint_commanded); const home = this.wasmHomeLimit.read(); const io = this.xyzacIo?.canonical?.(); const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null; const sourceProgram = program ?? this.xyzacProgram?.snapshot(); const spindle = ioState?.spindles?.[0] ?? null; const kinematics = this.xyzacKinematics?.snapshot?.(); const plannerJoints = wasmMotion.status === "verified" && Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : null; const kinematicsJoints = plannerJoints ?? (kinematics?.joints?.length === 5 ? [...kinematics.joints] : null); const unsigned = { schema: "sim_motion.v1", source: wasmMotion.state.source ?? "wasm_motion_queue", tick: wasmMotion.status === "verified" ? wasmMotion.state.tick : this.tick, axes: { x: position.x, y: position.y, z: position.z }, joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position[this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"] ?? 0, position.c ?? 0]), velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0, velocity_mm_min: Number(wasmMotion.state?.velocity_mm_min ?? 0), feed_command_mm_min: Number(sourceProgram?.feed ?? 0), feed_override: Number(wasmMotion.state?.feed_override ?? 1), spindle_override: Number(ioState?.spindles?.[0]?.override ?? 1), spindle_speed_rpm: Number(spindle?.speed_rpm ?? 0), spindle_command_rpm: Number(spindle?.command_rpm ?? 0), spindle_enabled: Boolean(spindle?.enabled), spindle_at_speed: Boolean(spindle?.at_speed), velocity_scale: Number(wasmMotion.state?.velocity_scale ?? 1), jog_scale: Number(wasmMotion.state?.jog_scale ?? 1), jog_velocity_linear: Number(wasmMotion.state?.jog_velocity_linear ?? 0), jog_velocity_angular: Number(wasmMotion.state?.jog_velocity_angular ?? 0), max_velocity: Number(wasmMotion.state?.velocity_limit ?? 0), homed: Boolean(home.state?.homed), homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(Number(home.state?.homed_mask ?? 0) & (1 << index))), limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), [this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"]: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) }, on_any_limit: Boolean(home.state?.limit_error), following_error: 0, motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (home.state?.homed ? "TELEOP" : "FREE"), homing: Boolean(home.state?.homing), jog: null, stepping: this.wasmMotion.isStepping?.() ?? false, 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) }; } syncXyzacProgramSpindle(program) { if (!this.xyzacIo || !program || typeof program !== "object") return { status: "skipped" }; const mode = Number(program.spindle_mode ?? 5); const rpm = Number(program.speed ?? 0); const enabled = (mode === 3 || mode === 4) && Number.isFinite(rpm) && rpm > 0; const signedRpm = mode === 4 ? -rpm : rpm; const current = this.xyzacIo.canonical()?.spindles?.[0]; if (current && Boolean(current.enabled) === enabled && Math.abs(Number(current.command_rpm ?? 0) - (enabled ? signedRpm * Number(current.override ?? 1) : 0)) < 1e-9) return { status: "skipped" }; return this.xyzacIo.command("SPINDLE_COMMAND", { spindle: 0, rpm: signedRpm, enabled }); } 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 coordinates = this.machineDefinition?.coordinates ?? "XYZAC"; const stem = coordinates.toLowerCase(); const rows = [[`remap_subs/${stem}_switchkins_sub.ngc`, `${stem}_switchkins_sub`], [`remap_subs/helix_${stem.slice(3)}.ngc`, `helix_${stem.slice(3)}`]]; 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, path); 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 synchronizedPosition = this.wasmMotion.read().state.position; const loaded = this.xyzacProgram.load(source, path); if (loaded.status !== "verified") return loaded; const synchronized = this.xyzacProgram.synchronizePosition(synchronizedPosition); if (synchronized.status !== "verified") return synchronized; 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" }; } clearLivePlot() { this.livePathEpoch += 1; return this.livePathEpoch; } 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(program) : 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 }); const xyzac = this.xyzacKinematics ? { coordinates: this.machineDefinition?.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 ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state, taskfile: Boolean(this.xyzacProgram?.path), queued_mdi_commands: task.queued_mdi_commands ?? 0, motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (homeLimit.state?.homed ? "TELEOP" : "FREE"), kinematics_type: "BOTH", spindle_direction: Number(devices.spindles?.[0]?.direction ?? 0), tool: Number(devices.tool?.current ?? 0), on_any_limit: motion.on_any_limit, paused: axis.operation_state === "PAUSED" }); if (homeLimit.state?.homing) { for (const name of ["run", "step", "load", "reload", "home", "jog", "mdi", "touchOff"]) ui.gates[name] = { action: name, enabled: false, reason: "linuxcnc_homing_active" }; } const gates = this.xyzacProgram ? { load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }), reload: this.xyzacProgram.path ? this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }) : { enabled: false, reason: "axis_taskfile_missing" }, 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 } }), jogScale: this.xyzacCommandGate({ kind: "SET_JOG_SCALE", payload: { scale: 1 } }), maxVelocity: this.xyzacCommandGate({ kind: "SET_MAX_VELOCITY", payload: { scale: 1 } }), spindleOverride: this.xyzacCommandGate({ kind: "SET_SPINDLE_OVERRIDE", payload: { scale: 1 } }), spindle: this.xyzacCommandGate({ kind: "SPINDLE_COMMAND", payload: {} }), coolant: this.xyzacCommandGate({ kind: "COOLANT_COMMAND", payload: {} }), tool: this.xyzacCommandGate({ kind: "TOOL_PREPARE", payload: {} }), probe: this.xyzacCommandGate({ kind: "PROBE_CONFIG", payload: {} }), touchOff: this.xyzacCommandGate({ kind: "TOUCH_OFF", payload: {} }), toolTouchOff: this.xyzacCommandGate({ kind: "TOOL_TOUCH_OFF", payload: {} }), 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" ? (Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position[this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"] ?? 0, position.c ?? 0]) : authoritative.kinematics.joints; const preview = this.previewState?.status === "verified" ? this.previewState : null; // Keep the Vismach model identity in the same authoritative visual // envelope as the canonical toolpath and live motion trace. The source // hash comes from the exact resource resolved during WASM startup, so a // renderer can audit that its geometry matches the loaded LinuxCNC case. const modelResource = this.configurationState?.exact_resources?.find?.((row) => row.role === "vismach_source"); const machineDefinition = this.machineDefinition; const modelSourcePath = this.machineDefinition?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py"; const modelSourceText = this.workspaceFiles?.[modelSourcePath]; const machineModel = this.xyzacProgram ? { schema: "linuxcnc-vismach-model.v1", id: machineDefinition?.id ?? "xyzac-trt", source: "linuxcnc-wasm-worker", source_path: modelResource?.path ?? modelSourcePath, source_hash: modelResource?.data_hash ?? this.visualModelSourceHash ?? (typeof modelSourceText === "string" ? fnv1a64(modelSourceText) : null), coordinate_system: machineDefinition?.coordinates ?? "XYZAC", geometry_variant: machineDefinition?.coordinates === "XYZBC" ? "moving-head-y-tilt" : "moving-knee-x-tilt", tilt_axis: machineDefinition?.coordinates === "XYZBC" ? "Y" : "X", rotary_axis: "Z", joints: machineDefinition?.coordinates ? ["table-x", "saddle-y", "spindle-z", `tilt-${machineDefinition.coordinates[3].toLowerCase()}`, "rotate-c"] : ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c"], components: ["base", "column", "head", "spindle", "tool", "table", "saddle", "zslide"], } : null; const visual = { schema: "axis-visual.v1", pose: { ...position }, joints: Array.isArray(joints) ? [...joints] : [], // Home changes the canonical current pose even when the simulation // tick does not advance. The renderer uses this monotonic source // sequence to reset its displayed pose without clearing Live Plot. pose_epoch: Number(homeLimit.state?.home_sequence ?? 0), live_path_epoch: this.livePathEpoch, ...(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", velocity_mm_min: motion.velocity_mm_min ?? 0, machine_limits: this.wasmMotion.axisLimits?.() ?? null, coordinate_offset: program?.coordinate_context?.offsets?.[(program?.coordinate_context?.system ?? 1) - 1]?.slice(0, 3) ?? null, ...(machineModel ? { machine_model: machineModel } : {}), }; const coordinateAxes = machineDefinition?.axes ?? ["x", "y", "z", "a", "c"]; const axisCoordinates = coordinateAxes.map((axis) => position[axis] ?? 0); const coordinateContext = program?.coordinate_context; const activeOffsets = coordinateContext?.offsets?.[coordinateContext.system - 1] ?? Array(9).fill(0); const displayedOffsets = coordinateAxes.map((axis) => activeOffsets[["x", "y", "z", "a", "b", "c", "u", "v", "w"].indexOf(axis)] ?? 0); const unsigned = { schema: "axis-panel-snapshot.v1", context_id: this.contextId, session_id: this.sessionId, simulation_tick: tick, task, program, motion, ...(this.xyzacProgram ? { source_status: { task: { mode: task.mode, state: axis.source_task_state, interpState: axis.source_interp_state }, traj: { mode: this.wasmHomeLimit.mode, motion_type: wasmMotion.state.motion_type }, homing_active: Boolean(homeLimit.state.homing), homing_history: this.homingHistory, joint: homeLimit.state.joint, authority: { task: "wasm-task-adapter", homing: "linuxcnc-homing.c", home_state: "joint.N.home-state" }, } } : {}), coordinates: { machine: [...axisCoordinates], relative: axisCoordinates.map((value, index) => value - displayedOffsets[index]), commanded: [...axisCoordinates], offsets: displayedOffsets, system: coordinateContext?.system ?? 1 }, 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, ...(this.xyzacProgram ? { ui_gates: { ...ui.gates, toolTouchOff: gates.toolTouchOff }, ui_relief: ui.relief } : {}), 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: { ...xyzac, coordinates: machineDefinition?.coordinates ?? xyzac.coordinates } } : {}), }; 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: this.machineDefinition?.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(); if (["HOME", "SET_MODE"].includes(input.kind) && Number(wasmMotion.state?.queue_depth) > 0) return { enabled: false, reason: "xyzac_motion_active" }; if (input.kind === "JOG_START" && !this.wasmHomeLimit.read().state.homed) return { enabled: false, reason: "xyzac_not_all_homed" }; if (input.kind === "PROGRAM_LOAD" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) { return { action: null, enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } }; } if (input.kind === "KINEMATICS_SELECT" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) { return { action: input.payload?.type === 0 ? "switchkinsIdentity" : input.payload?.type === 1 ? "switchkinsXyzac" : "switchkinsUserk", enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } }; } const homeLimit = this.wasmHomeLimit.read(); if (this.wasmHomeLimit.active && ["HOME", "JOG_START", "JOG_STOP", "MDI_EXECUTE", "PROGRAM_LOAD", "LOAD_WORKSPACE", "PROGRAM_RUN", "PROGRAM_STEP", "PROGRAM_RESTART", "PROGRAM_SEEK", "KINEMATICS_SELECT", "SET_MODE", "TOUCH_OFF", "RECOVER", "CHECKPOINT"].includes(input.kind)) { return { action: PANEL_GATE_ACTIONS[input.kind] ?? "home", enabled: false, reason: "xyzac_home_in_progress", taskState: task.state, interpState: "READING" }; } const estop = Boolean(io.estop); const ready = Boolean(io.ready) && !estop; const loaded = program.status !== 0; const action = PANEL_GATE_ACTIONS[input.kind] ?? null; if (input.kind === "TOOL_TOUCH_OFF") return { action: "toolTouchOff", enabled: false, reason: "blocked_by_abi_tool_offsets", taskState: task.state, interpState: "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status } }; const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" ? WASM_GATE_COMMANDS.JOG : input.kind === "SET_JOG_SCALE" || input.kind === "SET_MAX_VELOCITY" || input.kind === "SET_SPINDLE_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE : input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE : input.kind === "SPINDLE_COMMAND" || input.kind === "SPINDLE_CONSTANT" || input.kind === "SPINDLE_BRAKE" ? WASM_GATE_COMMANDS.SPINDLE : input.kind === "COOLANT_COMMAND" ? WASM_GATE_COMMANDS.COOLANT : input.kind === "TOOL_PREPARE" || input.kind === "TOOL_CHANGE" ? WASM_GATE_COMMANDS.TOOL : input.kind === "PROBE_CONFIG" ? WASM_GATE_COMMANDS.PROBE : input.kind === "TOUCH_OFF" ? WASM_GATE_COMMANDS.TOUCH_OFF : input.kind === "TOOL_TOUCH_OFF" ? WASM_GATE_COMMANDS.TOOL_TOUCH_OFF : 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 === "TOOL_TOUCH_OFF" ? Number(io.tool?.current ?? 0) > 0 ? 1 : 0 : 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 } }; } switchXyzacKinematics(mode) { if (!this.xyzacKinematics) return { status: "error", code: "xyzac_kinematics_unavailable" }; const current = this.wasmMotion.read(); if (current.status !== "verified") return { status: "error", code: "wasm_motion_unavailable" }; if (Number(current.state.queue_depth ?? 0) > 0) return { status: "error", code: "xyzac_motion_active" }; const joints = Array.isArray(current.state.joint_commanded) && current.state.joint_commanded.length === 5 ? [...current.state.joint_commanded] : [...this.xyzacKinematics.snapshot().joints]; const selected = this.xyzacKinematics.select(mode, joints); if (selected.status !== "verified") return selected; this.xyzacProgramKinematicsMode = mode; if (typeof this.wasmMotion.setActiveKinematicsTag === "function") this.wasmMotion.setActiveKinematicsTag(mode); if (selected.pose && typeof this.wasmMotion.setPosition === "function") this.wasmMotion.setPosition(selected.pose); return { ...selected, kinematics: this.xyzacKinematics.snapshot(), wasm_motion: this.wasmMotion.read() }; } xyzacTouchOff(input) { if (!this.xyzacProgram) return { status: "error", code: "xyzac_program_unavailable" }; const payload = input.payload ?? {}; const axis = String(payload.axis ?? "").trim().toUpperCase(); const coordinateAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"]; if (!coordinateAxes.includes(axis.toLowerCase())) return { status: "error", code: "touch_off_axis_invalid" }; const value = Number(payload.value); if (!Number.isFinite(value)) return { status: "error", code: "touch_off_value_invalid" }; const selectedSystem = String(payload.system ?? "P1 G54").trim().toUpperCase(); const systemMatch = selectedSystem.match(/^(P[0-9]|G(?:5[4-8]|59(?:\.[123])?))(?:\s+.*)?$/); if (!systemMatch) return { status: "error", code: "touch_off_system_invalid" }; const systemToken = systemMatch[1]; const systemNumber = systemToken.startsWith("P") ? Number(systemToken.slice(1)) : 1 + (Number(systemToken.slice(1, 3)) - 54) + (systemToken.includes(".") ? Number(systemToken.slice(-1)) : 0); if (!Number.isInteger(systemNumber) || systemNumber < 0 || systemNumber > 9) return { status: "error", code: "touch_off_system_invalid" }; const displayUnits = String(payload.units ?? "").toLowerCase(); const programUnits = Number(this.xyzacProgram.snapshot().units); const linear = ["X", "Y", "Z"].includes(axis); const converted = linear && displayUnits === "mm" && programUnits === 20 ? value / 25.4 : linear && displayUnits === "inch" && programUnits === 21 ? value * 25.4 : value; const formatted = Number(converted.toFixed(12)); const sourceBlock = `G10 L20 ${systemToken} ${axis}${formatted}`; // The LinuxCNC interpreter accepts the G54..G59 word in this command; // the selected WASM interpreter exposes the same operation through its // canonical P-number form (G54=P1, ..., G59.3=P9). const block = `G10 L20 P${systemNumber} ${axis}${formatted}`; const savedMode = this.task.state.mode; const savedMotionMode = this.wasmHomeLimit.mode; try { if (savedMode !== "MDI") this.task.setMode("MDI"); this.wasmHomeLimit.setMode("COORD"); this.wasmMotion.setPaused(false); const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position); if (synchronized.status !== "verified") return synchronized; const offset = this.xyzacProgram.executeBlock(block); if (offset.status !== "verified") return { ...offset, touch_off: { axis, value, system: selectedSystem, source_system: systemToken, source_block: sourceBlock, block } }; const preview = this.xyzacProgram.preview(); this.previewState = preview.status === "verified" ? preview : null; if (preview.status !== "verified") return preview; this.task.setProgram("loaded", this.xyzacProgram.snapshot().current_line); return { status: "verified", command: "TOUCH_OFF", source_block: sourceBlock, block, touch_off: { axis, value, system: selectedSystem, source_system: systemToken, source_block: sourceBlock, block }, program: this.xyzacProgram.snapshot(), motion: offset.motion ?? null }; } catch (error) { return { status: "error", code: String(error?.message ?? error) }; } finally { if (this.task.state.mode !== savedMode) this.task.setMode(savedMode); this.wasmHomeLimit.setMode(savedMotionMode); } } 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, live_path_epoch: this.livePathEpoch, 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(), live_path_epoch: this.livePathEpoch, ...(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.livePathEpoch = Number.isSafeInteger(state.live_path_epoch) ? state.live_path_epoch : this.livePathEpoch; 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 (Number.isSafeInteger(trace.state?.live_path_epoch)) this.livePathEpoch = trace.state.live_path_epoch; 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.livePathEpoch = Number.isSafeInteger(checkpoint.state.live_path_epoch) ? checkpoint.state.live_path_epoch : this.livePathEpoch; 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, { cycleTime: Number(checkpoint.state.wasm_motion.cycle_time ?? this.xyzacMotionCycleTime), velocityLimit: Number(checkpoint.state.wasm_motion.velocity_limit ?? 1e9), }); replayWasmMotion.restoreState(checkpoint.state.wasm_motion); if (checkpoint.state.wasm_home_limit) { replayHomeLimit = new WasmHomeLimit(this.core); 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; const recoveredLivePathEpoch = result.state?.live_path_epoch ?? checkpoint?.state?.live_path_epoch; if (Number.isSafeInteger(recoveredLivePathEpoch)) this.livePathEpoch = recoveredLivePathEpoch; 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 pausedStep = input.kind === "SIM_STEP" && this.xyzacProgram && (this.task.state.state === "PAUSED" || this.xyzacProgram.snapshot().status === 3); const eventTick = input.kind === "SIM_STEP" && Number.isSafeInteger(input.payload?.cycles) && !pausedStep ? 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) => { if (this.xyzacProgram && ["SPINDLE_COMMAND", "SPINDLE_BRAKE", "SPINDLE_CONSTANT"].includes(kind) && !payload?.source_keyboard && (kind !== "SPINDLE_COMMAND" || payload?.delta === undefined)) { try { if (this.task.state.mode !== "MANUAL") { this.task.setMode("MANUAL"); this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE"); } } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); } } 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.pendingXyzacMotion = null; this.pendingProgramControl = 0; this.pendingTaskSync = 0; this.pendingServoSync = 0; this.xyzacExecutionWaitNextMotion = false; this.xyzacExecutionWaitSamples = 0; this.xyzacLastEnqueuedKinematicsMode = 0; this.xyzacDeferredPhysicalSwitchMode = null; this.xyzacDeferredBlockedActiveId = 0; this.xyzacDeferredControlMotionType = 0; this.xyzacQueuedLookaheadTargetId = 0; this.xyzacAllowQueuedLookahead = false; this.xyzacLastServoActiveId = 0; this.xyzacLastQueuedMotion = null; this.xyzacDeferredPhysicalPose = null; this.xyzacDeferredPhysicalPoseApplied = false; this.xyzacAppendDeferredOnActive = false; this.wasmMotion.abort(); this.wasmMotion.setPaused(true); this.wasmHomeLimit.cancel(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.clearLivePlot(); 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, live_path_epoch: this.livePathEpoch, 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); if (this.xyzacProgram && mode === "MANUAL") this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE"); if (this.xyzacProgram && mode !== "MANUAL" && this.wasmHomeLimit.read().state.homed) this.wasmHomeLimit.setMode("COORD"); } 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.pendingXyzacMotion = null; this.pendingProgramControl = 0; this.pendingTaskSync = 0; this.pendingServoSync = 0; this.xyzacExecutionWaitNextMotion = false; this.xyzacExecutionWaitSamples = 0; this.xyzacLastEnqueuedKinematicsMode = 0; this.xyzacDeferredPhysicalSwitchMode = null; this.xyzacDeferredBlockedActiveId = 0; this.xyzacDeferredControlMotionType = 0; this.xyzacQueuedLookaheadTargetId = 0; this.xyzacAllowQueuedLookahead = false; this.xyzacLastServoActiveId = 0; this.xyzacLastQueuedMotion = null; this.xyzacDeferredPhysicalPose = null; this.xyzacDeferredPhysicalPoseApplied = false; this.xyzacAppendDeferredOnActive = false; this.wasmMotion.abort(); this.wasmMotion.setPaused(true); this.wasmHomeLimit.cancel(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.machineDefinition = machineDefinition(configuration.entrypoint) ?? machineDefinition(configuration.kinematics?.type) ?? this.machineDefinition; this.workspaceFiles = { ...(configuration.files ?? configuration.workspace ?? configuration.workspace_files ?? {}) }; const visualSourcePath = (this.machineDefinition ?? machineDefinition(configuration.entrypoint))?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py"; this.visualModelSourceHash = typeof this.workspaceFiles[visualSourcePath] === "string" ? fnv1a64(this.workspaceFiles[visualSourcePath]) : null; 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), }; } } const configuredMachine = this.machineDefinition ?? machineDefinition(configuration?.kinematics?.type); const isTrt = configuredMachine?.runtime === "trt" || configuration?.kinematics?.type === "xyzac-trt" || configuration?.kinematics?.type === "xyzbc-trt"; this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") || isTrt ? "wasm" : "js"; if (isTrt) { this.xyzacKinematics?.destroy(); this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, coordinates: configuredMachine?.coordinates ?? configuration.kinematics.coordinates ?? "XYZAC", 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.wasmMotion.destroy(); this.xyzacMotionCycleTime = Number(configuration.kinematics?.servo_cycle_time ?? configuration.kinematics?.cycle_time ?? 0.01); if (!Number.isFinite(this.xyzacMotionCycleTime) || this.xyzacMotionCycleTime <= 0) throw new Error("wasm_xyzac_cycle_time_invalid"); const taskCycleTime = Number(configuration.kinematics?.task_cycle_time ?? this.xyzacMotionCycleTime); this.xyzacTaskCycleTicks = Math.max(1, Math.round(taskCycleTime / this.xyzacMotionCycleTime)); // The selected XYZAC TRT case always uses the upstream LinuxCNC TP; // a caller must not silently downgrade it to the compatibility // motion queue by omitting the planner option. this.wasmMotion = isTrt ? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} }) : new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 }); this.xyzacProgram = new WasmProgramRuntime(this.core); this.homingHistory = []; this.xyzacProgramKinematicsMode = XYZAC_MODES.identity; if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics); this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35); const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20); const angularLimit = Number(configuration.kinematics?.axis_angular_velocity_limit ?? 30); const rotaryAxis = (configuredMachine?.coordinates ?? "XYZAC")[3].toLowerCase(); this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, [rotaryAxis]: angularLimit, c: angularLimit }; const accelerationLimit = Number(configuration.kinematics?.max_acceleration ?? 300); this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, [rotaryAxis]: accelerationLimit, c: accelerationLimit }; this.wasmMotion.setDynamics(Number(configuration.kinematics?.max_acceleration ?? 300), Number(configuration.kinematics?.max_jerk ?? 1000000000)); this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity); } if (configuration?.home_limits && this.motionAuthority === "wasm") { this.wasmHomeLimit.destroy(); this.wasmHomeLimit = this.xyzacProgram ? new WasmNativeHoming(this.core, configuration, this.xyzacKinematics) : 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" && isTrt)) { 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 homingSample = Boolean(this.xyzacProgram && this.wasmHomeLimit.active); 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"); // A poll may already be in flight when PROGRAM_PAUSE reaches the // worker. LinuxCNC keeps the paused task/TP state stable; accept that // transport frame as a no-op instead of rejecting it at the protocol // layer and leaving the next PROGRAM_RESUME with a sequence gap. if (this.xyzacProgram && (this.task.state.state === "PAUSED" || this.xyzacProgram.snapshot().status === 3) && !this.wasmMotion.isStepping()) { const pausedProgram = this.xyzacProgram.snapshot(); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, program_steps: [], execution_trace: [], snapshot_mode: input.payload?.response === "motion_delta" ? "motion_delta" : "full", program: pausedProgram, task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null, }), "verified"); } let program_steps = []; const motion_trace = []; let sourceLookaheadReady = false; try { this.scheduler.step(cycles, { task: (nextTick) => { if (this.xyzacProgram) { if (this.wasmHomeLimit.active) return; const wasmState = this.xyzacProgram.snapshot(); if (wasmState.status !== 2) return; if (this.pendingTaskSync !== 0) { this.pendingTaskSync = 0; return; } // LinuxCNC's task/interpreter blocks on a full trajectory // planner queue. Let the motion phase consume one segment // before reading another source line instead of converting // normal planner backpressure into a program fault. // TP_DEFAULT_QUEUE_SIZE includes the active TC slot. Leave // one slot available for the active segment before appending // another lookahead TC; the upstream queue otherwise rejects // the boundary insertion even though its reported depth is // one below the configured capacity. if (this.wasmMotion.read().state.queue_depth >= this.wasmMotion.queueCapacity - 1) return; if (this.pendingXyzacMotion) { const pendingMode = Number(this.pendingXyzacMotion.kinematics_mode ?? 0); if (this.xyzacAppendDeferredOnActive) return; const queueState = this.wasmMotion.read().state; const deferredControlReady = this.xyzacDeferredControlMotionType === 1 ? Number(queueState.active_id ?? 0) !== this.xyzacDeferredBlockedActiveId : Number(queueState.active_id ?? 0) === this.xyzacDeferredBlockedActiveId; const deferredSwitchMotion = this.xyzacDeferredPhysicalSwitchMode !== null && pendingMode === this.xyzacDeferredPhysicalSwitchMode && this.xyzacQueuedLookaheadTargetId === 0 && deferredControlReady; const queuedLookaheadReady = deferredSwitchMotion || (this.xyzacAllowQueuedLookahead && this.xyzacLastServoActiveId === this.xyzacQueuedLookaheadTargetId); // Same-mode segments may be queued while the current TC is // active. LinuxCNC TP needs that lookahead to blend G64 // paths; withholding it until the queue drains creates a // real zero-velocity servo sample between long runs. A // kinematics-mode change remains synchronized below and is // deliberately deferred until its existing TC boundary. const pendingModeChanged = pendingMode !== this.xyzacLastEnqueuedKinematicsMode; if (queueState.queue_depth !== 0 && pendingModeChanged && !queuedLookaheadReady) return; if (queuedLookaheadReady && !deferredSwitchMotion && this.xyzacAllowQueuedLookahead) { // The next segment is appended after the first servo // sample of the now-active target, matching LinuxCNC's // TP queue observation (q=1 on entry, q=2 thereafter). sourceLookaheadReady = true; return; } const pending = this.pendingXyzacMotion; this.pendingXyzacMotion = null; const enqueue = this.wasmMotion.enqueue(pending); if (enqueue.status === "error") throw new Error(enqueue.code); const modeChanged = pendingMode !== this.xyzacLastEnqueuedKinematicsMode; this.xyzacLastEnqueuedKinematicsMode = pendingMode; this.xyzacLastQueuedMotion = pending; if (this.planner === "linuxcnc-tp" && modeChanged && queueState.queue_depth !== 0) { this.xyzacAllowQueuedLookahead = true; this.xyzacQueuedLookaheadTargetId = Number(pending.source_line ?? 0); } if (queuedLookaheadReady && !modeChanged) { this.xyzacAllowQueuedLookahead = false; this.xyzacQueuedLookaheadTargetId = 0; } if (this.xyzacExecutionWaitNextMotion && pendingMode !== XYZAC_MODES.identity) { this.xyzacExecutionWaitNextMotion = false; this.xyzacExecutionWaitSamples = 1; } else if (pendingMode === XYZAC_MODES.identity) this.xyzacExecutionWaitNextMotion = false; if (this.planner !== "linuxcnc-tp" || !modeChanged || queueState.queue_depth !== 0) return; // Source TP performs one servo cycle on the newly active // segment before task lookahead appends the next XYZAC // segment. Keep parsing in this task, but defer that queue // mutation until the first servo sample below. sourceLookaheadReady = true; } // LinuxCNC's task planner consumes non-motion interpreter // blocks in the same task cycle and stops when the first // canonical motion is handed to TP. Preserve that boundary // instead of charging comments, parameter assignments, M429, // and G10 blocks as artificial motion ticks. for (let parserStep = 0; parserStep < 64; parserStep += 1) { if (this.pendingProgramControl !== 0) { const pendingControl = this.pendingProgramControl; const queueState = this.wasmMotion.read().state; const deferIdentitySwitch = pendingControl === 429 && this.planner === "linuxcnc-tp" && (Number(queueState.motion_type ?? 0) === 3 || (Number(queueState.motion_type ?? 0) === 1 && Number(queueState.progress ?? 0) >= 0.999999)); if (queueState.queue_depth !== 0 && !deferIdentitySwitch) return; if (queueState.queue_depth !== 0 && deferIdentitySwitch) { this.xyzacProgramKinematicsMode = XYZAC_MODES.identity; this.xyzacDeferredPhysicalSwitchMode = XYZAC_MODES.identity; this.xyzacDeferredBlockedActiveId = Number(queueState.active_id ?? 0); this.xyzacDeferredControlMotionType = Number(queueState.motion_type ?? 0); const queuedEnd = this.xyzacLastQueuedMotion?.end ?? queueState.position; const endInverse = this.xyzacKinematics.inverseMode(XYZAC_MODES.xyzac, queuedEnd); if (endInverse.status !== "verified") throw new Error(`wasm_switchkins_deferred_inverse_${endInverse.code ?? "error"}`); const physical = this.xyzacKinematics.selectPhysical(XYZAC_MODES.identity, endInverse.joints); if (physical.status !== "verified") throw new Error(`wasm_switchkins_deferred_${physical.code ?? "error"}`); if (typeof this.wasmMotion.setGoalPosition === "function" && physical.pose) this.wasmMotion.setGoalPosition(physical.pose); this.xyzacDeferredPhysicalPose = physical.pose; this.xyzacDeferredPhysicalPoseApplied = false; this.xyzacDeferredCompletionActiveId = Number(this.xyzacLastQueuedMotion?.source_line ?? 0); // Preparing the future identity goal must not switch // the active kinematics while the helix is running. const restored = this.xyzacKinematics.selectPhysical(XYZAC_MODES.xyzac, endInverse.joints); if (restored.status !== "verified") throw new Error(`wasm_switchkins_deferred_restore_${restored.code ?? "error"}`); this.pendingProgramControl = 0; this.pendingTaskSync = 0; continue; } const currentState = this.wasmMotion.read().state; const current = currentState.position; this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[pendingControl]; const rotaryAxis = this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"; const joints = Array.isArray(currentState.joint_commanded) && currentState.joint_commanded.length === 5 ? currentState.joint_commanded : [current.x, current.y, current.z, current[rotaryAxis] ?? 0, current.c ?? 0]; const selected = typeof this.xyzacKinematics.selectPhysical === "function" ? this.xyzacKinematics.selectPhysical(this.xyzacProgramKinematicsMode, joints) : this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, joints); if (selected.status !== "verified") throw new Error(`wasm_switchkins_control_${this.pendingProgramControl}`); if (this.planner === "linuxcnc-tp" && selected.pose) { this.wasmMotion.setPosition(selected.pose); if (typeof this.wasmMotion.setActiveKinematicsTag === "function") this.wasmMotion.setActiveKinematicsTag(this.xyzacProgramKinematicsMode); } this.pendingProgramControl = 0; if (this.planner === "linuxcnc-tp" && pendingControl === 428) this.xyzacExecutionWaitNextMotion = true; } const step = this.xyzacProgram.step(); if (step.status !== "verified") throw new Error(step.code); const spindleSync = this.syncXyzacProgramSpindle(step.program); if (spindleSync.status === "error") throw new Error(spindleSync.code ?? "wasm_program_spindle_sync_failed"); // Path control is produced by the WASM interpreter and // applied to the WASM trajectory queue before enqueue. const motionControlMode = Number(step.program?.motion_control_mode ?? 0); if (typeof this.wasmMotion.setTermCondition === "function") { const mode = motionControlMode === 0 ? "continuous" : motionControlMode === 1 ? "exact_path" : "exact_stop"; this.wasmMotion.setTermCondition(mode, Number(step.program?.path_tolerance ?? 0)); } else { const queueProfile = motionControlMode === 0 ? 0 : 1; if (this.wasmMotion.controls.profile !== queueProfile) this.wasmMotion.setProfile(queueProfile); } program_steps.push(step); if (step.has_control && [428, 429, 430].includes(step.control_code)) { this.pendingProgramControl = step.control_code; if (this.planner === "linuxcnc-tp") this.pendingTaskSync = 1; } if (step.motion) { step.motion.kinematics_mode = this.xyzacProgramKinematicsMode; step.motion.source_line = Number(step.program?.execution_source_line ?? step.line ?? step.motion.source_line ?? 0); step.motion.source_file = step.program?.execution_source_file ?? this.xyzacProgram.path; const linearNoop = step.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => step.motion.start[axis] === step.motion.end[axis]); if (!linearNoop) { // Commands issued by task become visible to motion on // the following task cycle, matching LinuxCNC's NML // handoff boundary and preventing a zero-time jump in // current_line/exec_state snapshots. const directDeferredMotion = this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && this.xyzacDeferredControlMotionType === 1 && Number(step.motion.kinematics_mode ?? 0) === XYZAC_MODES.identity; this.pendingXyzacMotion = step.motion; if (directDeferredMotion) { this.xyzacAppendDeferredOnActive = true; if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(2); } } return; } if (step.program?.status === 4) return; } return; } 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); // The selected LinuxCNC case has a 1 ms servo period and a // 10 ms task period. A public SIM_STEP represents one task // cycle, so advance the WASM motion queue by the configured // servo/task ratio while publishing one stable task snapshot. let wasmMotion = this.wasmMotion.read(); const programPaused = this.xyzacProgram?.snapshot().status === 3; for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) { if (this.xyzacProgram && this.wasmHomeLimit.active) { const home = this.wasmHomeLimit.step(); if (home.status !== "verified") throw new Error(home.code); const transition = home.state.joint.map((j) => `${j.home_state}:${Number(j.homing)}:${Number(j.homed)}`).join(","); if (this.homingHistory.at(-1)?.transition !== transition) this.homingHistory = [...this.homingHistory.slice(-63), { tick: home.state.tick, transition, joint: home.state.joint }]; // Coordinate synchronization after native motor-offset / // free_tp updates, not an artificial move to machine zero. this.wasmMotion.setActiveKinematicsTag(this.xyzacKinematics.activeMode); this.wasmMotion.setPosition(home.state.pose); wasmMotion = this.wasmMotion.read(); if (!home.state.homing) { const sync = this.xyzacProgram.synchronizePosition(home.state.pose); if (sync.status !== "verified") throw new Error(sync.code); } continue; } if (programPaused && !this.wasmMotion.isStepping()) break; if (this.wasmMotion.controls.paused && this.wasmMotion.read().state.queue_depth === 0) continue; if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") { this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3); } const queueBeforeServo = Number(typeof this.wasmMotion.queueDepth === "function" ? this.wasmMotion.queueDepth() : this.wasmMotion.read().state.queue_depth ?? 0); const beforeServoState = this.wasmMotion.read().state; let servoResult; servoResult = this.wasmMotion.step(); if (servoResult.status === "error" && servoResult.code !== "wasm_motion_step_-605") throw new Error(servoResult.code); if (servoResult.status === "verified") { wasmMotion = servoResult; this.xyzacLastServoActiveId = Number(servoResult.state.active_id ?? 0); const queuedEndpoint = this.xyzacLastQueuedMotion?.end; const endpointNear = queuedEndpoint && Math.abs(Number(beforeServoState.position?.x ?? 0) - Number(queuedEndpoint.x ?? 0)) < 1e-3 && Math.abs(Number(beforeServoState.position?.y ?? 0) - Number(queuedEndpoint.y ?? 0)) < 1e-3 && Math.abs(Number(beforeServoState.position?.z ?? 0) - Number(queuedEndpoint.z ?? 0)) < 1e-3; if (this.xyzacAppendDeferredOnActive && this.pendingXyzacMotion && Number(beforeServoState.active_id ?? 0) === this.xyzacDeferredCompletionActiveId && Number(beforeServoState.motion_type ?? 0) === 3 && (Number(beforeServoState.progress ?? 0) >= 0.999999 || (Number(beforeServoState.velocity ?? 0) <= 0.1 && endpointNear))) { if (!this.xyzacDeferredPhysicalPoseApplied && this.xyzacDeferredPhysicalPose) { this.wasmMotion.setPosition(this.xyzacDeferredPhysicalPose); this.xyzacDeferredPhysicalPoseApplied = true; } const deferredMotion = this.pendingXyzacMotion; const deferredEnqueue = this.wasmMotion.enqueue(deferredMotion); if (deferredEnqueue.status === "error") throw new Error(deferredEnqueue.code); this.xyzacLastEnqueuedKinematicsMode = Number(deferredMotion.kinematics_mode ?? 0); this.xyzacLastQueuedMotion = deferredMotion; this.xyzacAllowQueuedLookahead = true; this.xyzacQueuedLookaheadTargetId = Number(deferredMotion.source_line ?? 0); this.pendingXyzacMotion = null; this.xyzacAppendDeferredOnActive = false; if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(null); servoResult.state.queue_depth = 2; if (this.wasmMotion.lastState) this.wasmMotion.lastState.queue_depth = 2; } if (this.planner === "linuxcnc-tp" && this.xyzacExecutionWaitSamples > 0 && Number(servoResult.state.active_id ?? 0) !== 0) this.xyzacExecutionWaitSamples -= 1; if (this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && Number(servoResult.state.active_tag ?? -1) === this.xyzacDeferredPhysicalSwitchMode) { const physicalJoints = Array.isArray(servoResult.state.joint_commanded) ? servoResult.state.joint_commanded : []; const physical = this.xyzacKinematics.selectPhysical(this.xyzacDeferredPhysicalSwitchMode, physicalJoints); if (physical.status !== "verified") throw new Error(`wasm_switchkins_deferred_${physical.code ?? "error"}`); this.xyzacDeferredPhysicalSwitchMode = null; this.xyzacDeferredPhysicalPose = null; this.xyzacDeferredPhysicalPoseApplied = false; } if (this.planner === "linuxcnc-tp" && sourceLookaheadReady && servo === 0 && this.pendingXyzacMotion) { const lookaheadMode = Number(this.pendingXyzacMotion.kinematics_mode ?? 0); const lookahead = this.wasmMotion.enqueue(this.pendingXyzacMotion); if (lookahead.status === "error") throw new Error(lookahead.code); this.xyzacLastEnqueuedKinematicsMode = lookaheadMode; this.xyzacLastQueuedMotion = this.pendingXyzacMotion; this.pendingXyzacMotion = null; this.xyzacAllowQueuedLookahead = false; this.xyzacQueuedLookaheadTargetId = 0; sourceLookaheadReady = false; wasmMotion = this.wasmMotion.read(); } } } if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205); if (this.motionAuthority === "wasm") { if (wasmMotion.status === "verified") { if (!xyzacAuthority) { const position = this.motion.applyWasmPosition(wasmMotion.state.position, wasmMotion.state.velocity, nextTick); if (position.status === "error") throw new Error(position.code); } } 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.execution_source_line ?? program.current_line) : program.line); if (status === "completed" && this.task.state.state === "RUNNING" && (this.motionAuthority !== "wasm" || this.wasmMotion.read().state.queue_depth === 0)) this.task.transition("STOPPED"); if (this.xyzacProgram && this.task.state.mode === "MDI" && this.task.state.state === "RUNNING" && 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(), xyzac_timing: { servo_cycle_time: this.xyzacMotionCycleTime, task_cycle_ticks: this.xyzacTaskCycleTicks } }; if (program) snapshotState.program = program; const lightweightXyzac = Boolean(this.xyzacProgram); // A high-frequency XYZAC motion-delta is already backed by the WASM // state returned in this response. Persisting a full structured // snapshot for every task tick makes the browser replay store grow // quadratically because each snapshot contains the complete program // and runtime state. Explicit CHECKPOINT/trace/power-off requests // still materialize a durable snapshot through createCheckpoint(). const snapshot = input.payload?.trace_only === true || (lightweightXyzac && input.payload?.response === "motion_delta" && input.payload?.checkpoint !== true) ? null : this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, program_steps, execution_trace, homing_sample: homingSample, ...(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 ? { snapshot } : {}) }), ...(program ? { program } : {}), state_hash: snapshot?.state_hash ?? fnv1a64(snapshotState), })); } case "PROGRAM_LOAD": { if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed"); const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source; const savedMode = this.task.state.mode; const restoreMode = () => { if (savedMode !== "MANUAL" && this.task.state.mode !== savedMode) this.task.setMode(savedMode); }; // AXIS open_file_guts() enters MANUAL before the replacement setup, // then restores the tab-selected task mode before PLAN_OPEN. The // final restore below mirrors set_mode_from_tab after the UI refresh. try { if (savedMode !== "MANUAL") this.task.setMode("MANUAL"); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); } // PROGRAM_LOAD is a replacement transaction. Clear the previous // canonical preview before asking the WASM interpreter to parse the // new source, so a parse/subroutine error cannot re-publish the old // toolpath as if it belonged to the requested file. this.previewState = null; if (this.xyzacProgram) { this.pendingXyzacMotion = null; this.pendingProgramControl = 0; this.pendingTaskSync = 0; this.pendingServoSync = 0; this.xyzacExecutionWaitNextMotion = false; this.xyzacExecutionWaitSamples = 0; this.xyzacLastEnqueuedKinematicsMode = 0; this.xyzacDeferredPhysicalSwitchMode = null; this.xyzacDeferredBlockedActiveId = 0; this.xyzacDeferredControlMotionType = 0; this.xyzacQueuedLookaheadTargetId = 0; this.xyzacAllowQueuedLookahead = false; this.xyzacLastServoActiveId = 0; this.xyzacLastQueuedMotion = null; this.xyzacDeferredPhysicalPose = null; this.xyzacDeferredPhysicalPoseApplied = false; this.xyzacAppendDeferredOnActive = false; restoreMode(); const synchronizedPosition = this.wasmMotion.read().state.position; const result = this.xyzacProgram.load(source, input.payload?.path ?? null); if (result.status === "verified") { const synchronized = this.xyzacProgram.synchronizePosition(synchronizedPosition); if (synchronized.status !== "verified") { restoreMode(); return finish(this.error(input, "program_synch", synchronized.code), "failed"); } } if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity); if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(input.payload?.path); if (subroutines.status !== "verified") { restoreMode(); 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; // AXIS open_file_guts()/reload_file() leaves the task usable for // AUTO single-step after an earlier abort. Recover the task edge // before publishing the newly loaded interpreter state. if (this.task.state.state === "STOPPED") this.task.recover(); this.task.setProgram("loaded", 0); } restoreMode(); 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); } restoreMode(); 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; } restoreMode(); 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 previousProgramStatus = this.xyzacProgram.snapshot().program_status; const result = this.xyzacProgram.start(); if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); this.wasmHomeLimit.setMode("COORD"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); } return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, reopened_taskfile: ["completed", "aborted"].includes(previousProgramStatus), start_line: 0, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); } const result = this.program.start(); if (result.status === "verified") { 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": { if (this.xyzacProgram && input.kind === "PROGRAM_ABORT" && this.wasmHomeLimit.active) this.wasmHomeLimit.cancel(); const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort"; if (this.xyzacProgram) { // PAUSE/RESUME are interpreter and TP state transitions. They must // preserve the pending canonical block and queued lookahead so a // resume continues the exact WASM trajectory. ABORT is the only // edge that clears the task/TP handoff state. if (action === "abort") { this.pendingXyzacMotion = null; this.pendingProgramControl = 0; this.pendingTaskSync = 0; this.pendingServoSync = 0; this.xyzacExecutionWaitNextMotion = false; this.xyzacExecutionWaitSamples = 0; this.xyzacLastEnqueuedKinematicsMode = 0; this.xyzacDeferredPhysicalSwitchMode = null; this.xyzacDeferredBlockedActiveId = 0; this.xyzacDeferredControlMotionType = 0; this.xyzacQueuedLookaheadTargetId = 0; this.xyzacAllowQueuedLookahead = false; this.xyzacLastServoActiveId = 0; this.xyzacLastQueuedMotion = null; this.xyzacDeferredPhysicalPose = null; this.xyzacDeferredPhysicalPoseApplied = false; this.xyzacAppendDeferredOnActive = false; } const result = this.xyzacProgram[action](); if (result.status === "verified") { // sim_protocol_accept has already applied the transport edge; // calling sim_protocol_pause/resume a second time would return a // false ERR_STATE. The LinuxCNC WASM TP remains authoritative for // the actual pause/resume motion transition. if (action === "pause") { this.wasmMotion.setPaused(true); this.task.transition("PAUSED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("paused", program.execution_source_line ?? program.current_line); } else if (action === "resume") { this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram(program.status === 4 ? "completed" : program.status === 1 ? "loaded" : "running", program.execution_source_line ?? program.current_line); } else { this.wasmMotion.abort(); this.task.transition("STOPPED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("aborted", program.execution_source_line ?? program.current_line); } } return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); } const result = this.program[action](); if (result.status === "verified") { 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) { this.pendingXyzacMotion = null; this.pendingProgramControl = 0; this.pendingTaskSync = 0; this.pendingServoSync = 0; this.xyzacExecutionWaitNextMotion = false; this.xyzacExecutionWaitSamples = 0; this.xyzacLastEnqueuedKinematicsMode = 0; this.xyzacDeferredPhysicalSwitchMode = null; this.xyzacDeferredBlockedActiveId = 0; this.xyzacDeferredControlMotionType = 0; this.xyzacQueuedLookaheadTargetId = 0; this.xyzacAllowQueuedLookahead = false; this.xyzacLastServoActiveId = 0; this.xyzacLastQueuedMotion = null; this.xyzacDeferredPhysicalPose = null; this.xyzacDeferredPhysicalPoseApplied = false; this.xyzacAppendDeferredOnActive = false; const result = this.resetXyzacProgramToLoaded(); if (result.status === "verified") { this.wasmMotion.abort(); 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) { try { let current = this.xyzacProgram.snapshot(); let queueDepth = Number(this.wasmMotion.read().state?.queue_depth ?? 0); let stepPhase = "source_line_pause"; let stepWaitingForMotion = false; let stepSourceLine = current.execution_source_line ?? current.current_line ?? 0; let stepTargetId = null; let stepResult = null; let stepTrace = []; this.task.setMode("AUTO"); this.wasmHomeLimit.setMode("COORD"); if (this.wasmMotion.isStepping()) { const motion = this.wasmMotion.read(); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "motion_step_pending", step_waiting_for_motion: true, step_target_id: Number(motion.state.active_id ?? 0) || this.xyzacStepTargetId || null, program_steps: [], execution_trace: [], program: current, task: this.task.export(), wasm_motion: motion, motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified"); } if (queueDepth > 0) { const targetId = Number(this.wasmMotion.read().state.active_id ?? 0) || this.xyzacStepTargetId; this.xyzacStepTargetId = targetId; if (current.status === 2) { const paused = this.xyzacProgram.pause(); if (paused.status !== "verified") return finish(this.error(input, "program_pause", paused.code), "failed"); if (this.task.state.state === "READY") this.task.transition("RUNNING"); if (this.task.state.state === "RUNNING") this.task.transition("PAUSED"); this.task.setProgram("paused", current.current_line); current = this.xyzacProgram.snapshot(); } this.wasmMotion.stepLine(); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "motion_step", step_waiting_for_motion: true, step_target_id: targetId || null, step_source_line: current.execution_source_line ?? current.current_line, program_steps: [], execution_trace: [], program: current, task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified"); } if (["completed", "aborted"].includes(current.program_status) && this.task.state.state === "STOPPED") { const reset = this.resetXyzacProgramToLoaded(); if (reset.status !== "verified") return finish(this.error(input, "program_step", reset.code ?? "program_reload_failed"), "failed"); this.wasmMotion.setPaused(false); this.xyzacProgramKinematicsMode = XYZAC_MODES.identity; if (typeof this.wasmMotion.setActiveKinematicsTag === "function") this.wasmMotion.setActiveKinematicsTag(XYZAC_MODES.identity); this.task.recover(); this.task.setProgram("loaded", 0); current = this.xyzacProgram.snapshot(); } const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state) && ![2, 3].includes(current.status); if (idle) { const started = this.xyzacProgram.start(); if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed"); stepPhase = "start_pause"; stepSourceLine = 0; } else { if (current.status === 3) { const resumed = this.xyzacProgram.resume(); if (resumed.status !== "verified") return finish(this.error(input, "program_resume", resumed.code), "failed"); current = this.xyzacProgram.snapshot(); if (current.status === 4) { this.task.setProgram("completed", current.current_line); if (["RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED"); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "terminal", step_waiting_for_motion: false, step_source_line: current.current_line, program_steps: [], execution_trace: [], program: current, task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified"); } if (current.status === 1) { const started = this.xyzacProgram.start(); if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed"); } } const stepped = this.xyzacProgram.step(); if (stepped.status !== "verified") return finish(this.error(input, "program_step", stepped.code), "failed"); stepResult = stepped; const sync = this.syncXyzacProgramSpindle(stepped.program); if (sync.status !== "verified" && sync.status !== "skipped") return finish(this.error(input, "program_spindle", sync.code), "failed"); if (stepped.has_control && [428, 429, 430].includes(stepped.control_code)) { const selected = this.switchXyzacKinematics({ 428: 1, 429: 0, 430: 2 }[stepped.control_code]); if (selected.status !== "verified") return finish(this.error(input, "program_step", selected.code), "failed"); } if (stepped.motion) { stepped.motion.kinematics_mode = this.xyzacProgramKinematicsMode; const queued = this.wasmMotion.enqueue(stepped.motion); if (queued.status === "error") return finish(this.error(input, "motion_queue", queued.code), "failed"); // AUTO_STEP releases the current trajectory line. The WASM TP // will stop again when its execution ID changes. this.wasmMotion.stepLine(); stepTargetId = Number(stepped.motion.source_line ?? stepped.line ?? 0) || Number(this.wasmMotion.read().state.active_id ?? 0) || null; this.xyzacStepTargetId = stepTargetId ?? 0; stepPhase = "source_line_motion"; stepWaitingForMotion = true; } stepSourceLine = stepped.program?.execution_source_line ?? stepped.line ?? stepSourceLine; stepTrace = [{ line: stepSourceLine, text: stepped.program?.execution_line_text ?? "", program_status: stepped.program?.program_status ?? null }].filter((entry) => Number.isSafeInteger(entry.line) && entry.line > 0); } if (this.xyzacProgram.snapshot().status === 4 && this.wasmMotion.read().state.queue_depth === 0) { this.task.setProgram("completed", this.xyzacProgram.snapshot().current_line); if (["RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED"); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: "terminal", step_waiting_for_motion: false, step_source_line: this.xyzacProgram.snapshot().current_line, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified"); } const paused = this.xyzacProgram.pause(); if (paused.status !== "verified") return finish(this.error(input, "program_pause", paused.code), "failed"); if (!this.wasmMotion.isStepping()) this.wasmMotion.setPaused(true); if (this.task.state.state === "STOPPED") this.task.recover(); if (this.task.state.state === "READY") this.task.transition("RUNNING"); if (this.task.state.state === "RUNNING") this.task.transition("PAUSED"); this.task.setProgram("paused", this.xyzacProgram.snapshot().current_line); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, step_phase: stepPhase, step_waiting_for_motion: stepWaitingForMotion || this.wasmMotion.isStepping(), step_source_line: stepSourceLine, step_target_id: stepTargetId, program_steps: stepResult ? [stepResult] : [], execution_trace: stepTrace, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified"); } catch (error) { return finish(this.error(input, "program_step", String(error?.message ?? error)), "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"); this.wasmHomeLimit.setMode("COORD"); // AXIS send_mdi_command() leaves the interpreter/TP runnable after // a previous manual jog stop. JOG_STOP intentionally pauses the // WASM TP, so clear that transport pause before queuing MDI motion; // the queue itself remains authoritative for execution. this.wasmMotion.setPaused(false); const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position); if (synchronized.status !== "verified") return finish(this.error(input, "mdi_synch", synchronized.code), "failed"); 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.switchXyzacKinematics(mode); if (selected.status !== "verified") return finish(this.error(input, "kinematics", selected.code ?? "xyzac_switchkins_failed"), "failed"); result.kinematics = selected.kinematics; 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"); if (this.wasmMotion.read().state.queue_depth > 0 && this.task.state.state !== "RUNNING") this.task.transition("RUNNING"); } } 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 "TOUCH_OFF": case "TOOL_TOUCH_OFF": { if (input.kind === "TOOL_TOUCH_OFF") return finish(this.error(input, "abi_capability", "blocked_by_abi_tool_offsets"), "failed"); const result = this.xyzacTouchOff(input); return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), motion_authority: this.xyzacProgram ? "wasm" : this.motionAuthority, 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"); this.wasmHomeLimit.setMode("COORD"); const result = this.switchXyzacKinematics(mode); 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: result.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 payload = input.payload ?? {}; // Older callers used HOME with an empty payload for Home All. Keep // that compatible while making the explicit Web contract // HOME {all:true}. const all = payload.all === true || (payload.all === undefined && payload.axis === undefined && payload.joint === undefined); if (all && payload.joint !== undefined) return finish(this.error(input, "home_limit", "home_target_ambiguous"), "failed"); if (payload.axis !== undefined) return finish(this.error(input, "home_limit", "home_joint_requires_joint_mode"), "failed"); const requestedJoint = payload.joint; if (!all && requestedJoint === undefined) return finish(this.error(input, "home_limit", "home_joint_required"), "failed"); if (!all && (!Number.isInteger(requestedJoint) || requestedJoint < 0 || requestedJoint >= 5)) return finish(this.error(input, "home_limit", "home_joint_invalid"), "failed"); // AXIS home_all_joints()/home_joint() calls ensure_mode(MANUAL) // before go_home() selects FREE/joint and issues c.home(). try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); } try { this.wasmHomeLimit.sync(this.wasmMotion.read().state.joint_commanded); const home = this.wasmHomeLimit.home(input.seq, all ? null : requestedJoint); this.homingHistory = [{ tick: home.state.tick, transition: "command", joint: home.state.joint }]; return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, homing: { phase: "accepted", joint: all ? -1 : requestedJoint, source: "linuxcnc-homing.c" }, wasm_home_limit: home, motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified"); } catch (error) { return finish(this.error(input, "home_limit", error.message), "failed"); } } 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_JOG_SCALE": case "SET_MAX_VELOCITY": case "SET_OVERRIDE": case "FOLLOWING_ERROR": { if (this.motionAuthority === "wasm") { if (input.kind === "JOG_STOP") { this.wasmMotion.abort(); // AXIS jog_off() stops the active jog, but it does not leave the // task/TP in AUTO_PAUSE. Keep the LinuxCNC TP runnable so an // already queued servo sample, MDI block, or spindle/IO command // cannot surface a synthetic linuxcnc_tp_paused scheduler error. this.wasmMotion.setPaused(false); 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 === "SET_JOG_SCALE" || input.kind === "SET_MAX_VELOCITY") { const scale = Number(input.payload?.scale ?? Number(input.payload?.percent) / 100); if (!Number.isFinite(scale) || scale < 0.06 || scale > 1) return finish(this.error(input, "motion_scale", input.kind === "SET_JOG_SCALE" ? "jog_scale_range" : "max_velocity_scale_range"), "failed"); try { if (input.kind === "SET_JOG_SCALE") this.wasmMotion.setJogScale(scale); else this.wasmMotion.setVelocityScale(scale); } catch (error) { return finish(this.error(input, "motion_scale", String(error?.message ?? error)), "failed"); } const motion = this.wasmMotionProjection(); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, 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 axis = String(input.payload?.axis ?? "").toLowerCase(); const availableAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"]; if (!availableAxes.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"); if (this.xyzacProgram) this.wasmHomeLimit.setMode("TELEOP"); this.wasmMotion.setPaused(false); const requestedIncrement = input.payload?.increment; const continuous = requestedIncrement === null || input.payload?.continuous === true; const displayScale = ["x", "y", "z"].includes(axis) && input.payload?.units === "inch" ? 25.4 : 1; const distance = continuous ? 0 : Number(requestedIncrement ?? 1) * displayScale; if (!continuous && (!Number.isFinite(distance) || distance <= 0)) return finish(this.error(input, "motion_jog", "motion_jog_nonfinite"), "failed"); // The selected XYZAC case uses the LinuxCNC TP jog ABI. Keep the // existing queue-backed WASM cases working as well: they have no // native jog entry point, so represent one incremental/continuous // sample as a normal queued line and retain the same JOG_STOP abort // path. This compatibility branch is never used by XYZAC TRT. const jog = typeof this.wasmMotion.jog === "function" ? this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, b: 4, c: 5 }[axis], direction, distance, input.seq) : (() => { const current = this.wasmMotion.read(); const start = current.state?.position ?? {}; const end = { ...start, [axis]: Number(start[axis] ?? 0) + direction * (continuous ? Math.max(Number(input.payload?.speed ?? 1) * this.xyzacMotionCycleTime * 5, Number.EPSILON) : distance) }; return this.wasmMotion.enqueue({ start, end, velocity: Math.max(Number(input.payload?.speed ?? 1), Number.EPSILON), acceleration: 1, motion_type: 1, source_line: input.seq }); })(); if (jog.status !== "verified") { const code = jog.code === "linuxcnc_tp_jog_-1217" ? "motion_limit_range" : jog.code === "linuxcnc_tp_jog_-1215" ? "motion_jog_nonfinite" : jog.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 "SET_SPINDLE_OVERRIDE": case "SPINDLE_COMMAND": case "SPINDLE_CONSTANT": case "SPINDLE_BRAKE": 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 };