import { fnv1a64 } from "./replay.mjs"; const IO_SCHEMA = "wasm_xyzac_io.v1"; const HAL_SCHEMA = "wasm_xyzac_hal.v1"; const POSE_BYTES = 72; const HAL_WRITE_ALLOWLIST = new Set(["motion.probe-input", "iocontrol.0.tool-prepared", "iocontrol.0.tool-changed", "spindle.0.speed-in", "spindle.0.at-speed"]); const IO_ERROR_REASONS = Object.freeze({ [-900]: "wasm_io_invalid_argument", [-901]: "wasm_io_range_error", [-902]: "wasm_io_invalid_state", [-903]: "wasm_io_power_required", [-904]: "wasm_io_estop_active", [-905]: "wasm_io_fault_active", [-906]: "wasm_io_limit_active", [-907]: "wasm_io_nonfinite_value", [-908]: "wasm_io_hal_pin_error", [-909]: "wasm_io_capacity_exceeded", [-910]: "wasm_io_busy", [-911]: "wasm_io_probe_error", [-912]: "wasm_io_timeout", [-913]: "wasm_io_recovery_required", }); function call(core, name, returnType, argTypes, args) { const fn = core?.[`_${name}`] ?? core?.[name]; if (typeof fn === "function") return fn(...args); if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args); throw new Error(`missing core export: ${name}`); } function callString(core, name, returnType, argTypes, args) { if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args); throw new Error(`missing string-capable core export: ${name}`); } function readString(core, pointer, capacity) { const bytes = core.HEAPU8.subarray(pointer, pointer + capacity); const end = bytes.indexOf(0); return new TextDecoder().decode(bytes.subarray(0, end < 0 ? bytes.length : end)); } function bytesToHex(bytes) { let output = ""; for (const byte of bytes) output += byte.toString(16).padStart(2, "0"); return output; } function hexToBytes(value) { if (typeof value !== "string" || value.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(value)) throw new Error("wasm_xyzac_io_state_bytes_invalid"); const bytes = new Uint8Array(value.length / 2); for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16); return bytes; } export class WasmXyzacIo { constructor(core, { spindleCount = 2, toolCapacity = 1001 } = {}) { this.core = core; this.available = ["sim_io_size", "sim_io_init", "sim_io_canonical", "sim_io_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function"); this.pointer = 0; this.posePointer = 0; this.ioSize = 0; this.spindleCount = spindleCount; this.toolCapacity = toolCapacity; this.lastError = 0; if (!this.available || typeof core?._malloc !== "function") return; this.ioSize = call(core, "sim_io_size", "number", [], []); this.pointer = core._malloc(this.ioSize); this.posePointer = core._malloc(POSE_BYTES); if (!this.pointer || !this.posePointer) throw new Error("wasm_xyzac_io_allocation_failed"); const result = call(core, "sim_io_init", "number", ["number", "number", "number", "number", "number", "number"], [this.pointer, spindleCount, toolCapacity, 0.01, 600, 5]); if (result !== 0) throw new Error(`wasm_xyzac_io_init_${result}`); } invoke(name, args = []) { if (!this.available) return { status: "blocked", code: "wasm_xyzac_io_unavailable" }; const result = call(this.core, name, "number", ["number", ...args.map(() => "number")], [this.pointer, ...args]); this.lastError = result; return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${name}_${result}`, result, reason: IO_ERROR_REASONS[result] ?? `wasm_${name}_${result}`, message_key: IO_ERROR_REASONS[result] ?? `wasm_${name}_${result}`, source: "linuxcnc-wasm-io" }; } canonical() { if (!this.available) return { status: "blocked", code: "wasm_xyzac_io_unavailable" }; const required = call(this.core, "sim_io_canonical", "number", ["number", "number", "number"], [this.pointer, 0, 0]); const buffer = this.core._malloc(required); try { call(this.core, "sim_io_canonical", "number", ["number", "number", "number"], [this.pointer, buffer, required]); return JSON.parse(readString(this.core, buffer, required)); } finally { this.core._free(buffer); } } stateHash() { if (!this.available) return null; const hash = call(this.core, "sim_io_state_hash", "number", ["number"], [this.pointer]); return `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`; } export() { const state = this.canonical(); if (state.status === "blocked") return { schema: IO_SCHEMA, source: "wasm_io", ...state }; const unsigned = { schema: IO_SCHEMA, source: "wasm_io", state, state_hash: this.stateHash(), state_bytes: bytesToHex(this.core.HEAPU8.slice(this.pointer, this.pointer + this.ioSize)) }; return { ...unsigned, projection_hash: fnv1a64(unsigned) }; } restoreState(value) { if (!value || !value.state_bytes || !this.available) throw new Error("wasm_xyzac_io_restore_unavailable"); const bytes = hexToBytes(value.state_bytes); if (bytes.length !== this.ioSize) throw new Error("wasm_xyzac_io_restore_layout"); this.core.HEAPU8.set(bytes, this.pointer); const actual = this.canonical(); if (value.state && fnv1a64(actual) !== fnv1a64(value.state)) throw new Error("wasm_xyzac_io_restore_state_mismatch"); return this.export(); } halExport() { if (!this.available) return { schema: HAL_SCHEMA, source: "wasm_hal", status: "blocked", code: "wasm_hal_unavailable" }; const tick = call(this.core, "sim_hal_tick", "number", [], []); const unsigned = { schema: HAL_SCHEMA, source: "wasm_hal", status: "verified", tick, pin_count: call(this.core, "sim_hal_pin_count", "number", [], []), param_count: call(this.core, "sim_hal_param_count", "number", [], []), thread_count: call(this.core, "sim_hal_thread_count", "number", [], []), function_count: call(this.core, "sim_hal_function_count", "number", [], []), component_count: call(this.core, "sim_hal_component_count", "number", [], []), }; return { ...unsigned, state_hash: fnv1a64(unsigned) }; } halValue(name, value, type = 4, param = false) { if (!this.available) return { status: "blocked", code: "wasm_hal_unavailable" }; if (param || !HAL_WRITE_ALLOWLIST.has(name)) return { status: "error", code: "wasm_hal_write_denied", name, target: param ? "param" : "pin" }; const result = callString(this.core, param ? "sim_hal_set_param_scalar" : "sim_hal_set_pin_scalar", "number", ["string", "number", "number"], [name, type, Number(value)]); return result === 0 ? { status: "verified", command: param ? "HAL_SET_PARAM" : "HAL_SET_PIN" } : { status: "error", code: `wasm_hal_set_${result}`, result }; } halCommand(payload = {}) { if (payload.action === "thread") return callString(this.core, "sim_hal_register_thread", "number", ["string", "number"], [payload.name ?? "servo-thread", payload.period_ticks ?? 1]) === 0 ? { status: "verified", command: "HAL_THREAD" } : { status: "error", code: "wasm_hal_thread" }; if (payload.action === "component") return callString(this.core, "sim_hal_register_component", "number", ["string", "string", "string"], [payload.instance ?? payload.name, payload.type ?? payload.module, payload.thread ?? "servo-thread"]) === 0 ? { status: "verified", command: "HAL_COMPONENT" } : { status: "error", code: "wasm_hal_component" }; if (payload.action === "connect") return callString(this.core, "sim_hal_connect", "number", ["string", "string"], [payload.source, payload.target]) === 0 ? { status: "verified", command: "HAL_CONNECT" } : { status: "error", code: "wasm_hal_connect" }; if (payload.action === "step") { const result = call(this.core, "sim_hal_step", "number", ["number"], [payload.ticks ?? payload.tick ?? 1]); return result === 0 ? { status: "verified", command: "HAL_STEP" } : { status: "error", code: `wasm_hal_step_${result}` }; } return { status: "error", code: "wasm_hal_command_unsupported" }; } command(kind, payload = {}) { if (kind === "RESET") return this.invoke("sim_io_reset"); if (kind === "SPINDLE_CONSTANT") return { status: "verified", command: "SPINDLE_CONSTANT", source: "linuxcnc_spindle_constant" }; if (kind === "SPINDLE_COMMAND") { if (payload.delta !== undefined) return this.invoke("sim_io_spindle_adjust", [payload.spindle ?? 0, Number(payload.delta)]); const requestedRpm = Number(payload.rpm ?? payload.speed ?? 0); const signedRpm = payload.direction === -1 ? -requestedRpm : payload.direction === 1 ? requestedRpm : requestedRpm; return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, signedRpm, payload.enabled === false ? 0 : 1]); } if (kind === "SPINDLE_BRAKE") return this.invoke("sim_io_spindle_brake", [payload.spindle ?? 0, payload.engaged === true ? 1 : 0]); if (kind === "SPINDLE_OVERRIDE" || kind === "SET_SPINDLE_OVERRIDE") return this.invoke("sim_io_spindle_set_override", [payload.spindle ?? 0, Number(payload.scale ?? payload.override ?? 1)]); if (kind === "SPINDLE_FAULT") return this.invoke("sim_io_spindle_set_fault", [payload.spindle ?? 0, payload.fault === true ? 1 : 0]); if (kind === "SPINDLE_INHIBIT") return this.invoke("sim_io_spindle_set_inhibit", [payload.spindle ?? 0, payload.inhibit === true ? 1 : 0]); if (kind === "SPINDLE_ORIENT") return this.invoke("sim_io_spindle_orient", [payload.spindle ?? 0, payload.angle ?? 0, payload.mode ?? 0]); if (kind === "SPINDLE_ORIENTED") return this.invoke("sim_io_spindle_set_oriented", [payload.spindle ?? 0, payload.oriented === true ? 1 : 0, payload.fault ?? 0]); if (kind === "TOOL_PREPARE") return this.invoke("sim_io_tool_prepare", [payload.tool ?? payload.tool_number ?? 0, payload.pocket ?? payload.pocket_number ?? 0]); if (kind === "TOOL_PREPARED") return this.invoke("sim_io_tool_set_prepared", [payload.prepared === false ? 0 : 1]); if (kind === "TOOL_CHANGE") return this.invoke("sim_io_tool_change"); if (kind === "TOOL_CHANGED") return this.invoke("sim_io_tool_set_changed", [payload.changed === false ? 0 : 1]); if (kind === "TOOL_ABORT") return this.invoke("sim_io_tool_abort", [payload.reason ?? 0]); if (kind === "TOOL_FAULT") return this.invoke("sim_io_tool_set_fault", [payload.fault === true ? 1 : 0, payload.reason ?? 0]); if (kind === "TOOL_CLEAR_FAULT") return this.invoke("sim_io_tool_clear_fault"); if (kind === "PROBE_CONFIG") return payload.action === "disarm" ? this.invoke("sim_io_probe_disarm") : this.invoke("sim_io_probe_arm", [payload.probe_type ?? payload.type ?? 0]); if (kind === "ESTOP" || kind === "DEVICE_ESTOP") return this.invoke("sim_io_set_estop", [payload.active === true ? 1 : 0]); if (kind === "POWER") return this.invoke("sim_io_set_power", [payload.active === true ? 1 : 0]); if (kind === "IO_RECOVER" || kind === "INTERLOCK_RESET") return this.invoke("sim_io_recover"); if (kind === "IO_STEP") return this.invoke("sim_io_step", [payload.ticks ?? 1]); if (kind === "COOLANT_COMMAND") return { status: "error", code: "wasm_device_io_required", detail: "coolant uses sim_device_io authority" }; return { status: "error", code: "wasm_xyzac_io_command_unsupported", command: kind }; } probeUpdate(pose, input = 1) { if (!this.available) return { status: "blocked", code: "wasm_xyzac_io_unavailable" }; const values = ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => Number(pose?.[axis] ?? 0)); this.core.HEAPF64.set(values, this.posePointer / 8); return this.invoke("sim_io_probe_update", [this.posePointer, input ? 1 : 0]); } destroy() { for (const pointer of [this.pointer, this.posePointer]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer); this.pointer = this.posePointer = 0; } } export { HAL_WRITE_ALLOWLIST };