import { fnv1a64 } from "./replay.mjs"; import { WEB_SETTINGS } from "../workspace/web-settings.mjs"; const DEVICE_SCHEMA = "wasm_xyzac_device_io.v1"; const DEVICE_ERROR_REASONS = Object.freeze({ [-1000]: "wasm_device_invalid_argument", [-1001]: "wasm_device_invalid_state", [-1002]: "wasm_device_range_error", [-1003]: "wasm_device_estop_active", [-1004]: "wasm_device_interlock_open", [-1005]: "wasm_device_fault_active", [-1006]: "wasm_device_ack_missing", [-1007]: "wasm_device_ack_timeout", [-1008]: "wasm_device_ack_invalid", }); 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 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_device_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; } function operationNumber(value) { if (Number.isSafeInteger(value) && value > 0) return value; const text = String(value ?? "device"); let hash = 2166136261; for (const byte of new TextEncoder().encode(text)) { hash ^= byte; hash = Math.imul(hash, 16777619); } return (hash >>> 0) || 1; } export class WasmXyzacDeviceIo { constructor(core, { encoderCount = WEB_SETTINGS.runtime.io.encoder_count, cycleTime = WEB_SETTINGS.runtime.io.cycle_time } = {}) { this.core = core; this.available = ["sim_device_io_size", "sim_device_init", "sim_device_canonical", "sim_device_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function"); this.pointer = 0; if (!this.available || typeof core?._malloc !== "function") return; const size = call(core, "sim_device_io_size", "number", [], []); this.pointer = core._malloc(size); if (!this.pointer) throw new Error("wasm_xyzac_device_io_allocation_failed"); const result = call(core, "sim_device_init", "number", ["number", "number", "number"], [this.pointer, encoderCount, cycleTime]); if (result !== 0) throw new Error(`wasm_xyzac_device_io_init_${result}`); } invoke(name, args = []) { if (!this.available) return { status: "blocked", code: "wasm_xyzac_device_io_unavailable" }; const result = call(this.core, name, "number", ["number", ...args.map(() => "number")], [this.pointer, ...args]); const reason = DEVICE_ERROR_REASONS[result] ?? `wasm_${name}_${result}`; return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${name}_${result}`, result, reason, message_key: reason, source: "linuxcnc-wasm-device-io" }; } canonical() { if (!this.available) return { status: "blocked", code: "wasm_xyzac_device_io_unavailable" }; const required = call(this.core, "sim_device_canonical", "number", ["number", "number", "number"], [this.pointer, 0, 0]); const buffer = this.core._malloc(required); try { call(this.core, "sim_device_canonical", "number", ["number", "number", "number"], [this.pointer, buffer, required]); return JSON.parse(readString(this.core, buffer, required)); } finally { this.core._free(buffer); } } export() { const state = this.canonical(); if (state.status === "blocked") return { schema: DEVICE_SCHEMA, source: "wasm_device_io", ...state }; const unsigned = { schema: DEVICE_SCHEMA, source: "wasm_device_io", state, wasm_state_hash: this.stateHash(), state_bytes: bytesToHex(this.core.HEAPU8.slice(this.pointer, this.pointer + call(this.core, "sim_device_io_size", "number", [], []))) }; return { ...unsigned, state_hash: fnv1a64(unsigned) }; } restoreState(value) { if (!value || !value.state_bytes || !this.available) throw new Error("wasm_xyzac_device_io_restore_unavailable"); const bytes = hexToBytes(value.state_bytes); const size = call(this.core, "sim_device_io_size", "number", [], []); if (bytes.length !== size) throw new Error("wasm_xyzac_device_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_device_io_restore_state_mismatch"); return this.export(); } stateHash() { if (!this.available) return null; const hash = call(this.core, "sim_device_state_hash", "number", ["number"], [this.pointer]); return `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`; } command(kind, payload = {}, operationId = null) { if (kind === "ENCODER_SET") { const channel = payload.channel ?? 0; if (payload.speed_rps !== undefined || payload.speed !== undefined) return this.invoke("sim_device_encoder_set_speed", [channel, payload.speed_rps ?? payload.speed]); if (payload.ppr !== undefined || payload.scale !== undefined) return this.invoke("sim_device_encoder_set_config", [channel, payload.ppr ?? 1000, payload.scale ?? 1]); return this.invoke("sim_device_encoder_set_index_enable", [channel, payload.index_enable === true ? 1 : 0]); } if (kind === "COOLANT_COMMAND") return this.invoke("sim_device_coolant_command", [payload.mist ? 1 : 0, payload.flood ? 1 : 0, payload.lube ? 1 : 0]); if (kind === "COOLANT_FAULT") return this.invoke("sim_device_coolant_set_fault", [payload.fault === true ? 1 : 0]); if (kind === "DOOR_SET") return this.invoke("sim_device_door_set", [payload.open === true ? 1 : 0]); if (kind === "INTERLOCK_SET") return this.invoke("sim_device_interlock_set", [payload.input_ok === false ? 0 : 1]); if (kind === "INTERLOCK_RESET") return this.invoke("sim_device_interlock_reset"); if (kind === "DEVICE_ESTOP") return this.invoke("sim_device_estop_set", [payload.active === true ? 1 : 0]); if (kind === "DEVICE_ACK_ISSUE") return this.invoke("sim_device_ack_issue", [operationNumber(operationId ?? payload.operation_id), payload.timeout_ticks ?? 10]); if (kind === "DEVICE_ACK") return this.invoke("sim_device_ack_ack", [operationNumber(payload.operation_id)]); if (kind === "DEVICE_ACK_ABORT") return this.invoke("sim_device_ack_abort", [operationNumber(payload.operation_id)]); if (kind === "IO_STEP") return this.invoke("sim_device_step", [payload.ticks ?? 1]); return { status: "error", code: "wasm_xyzac_device_io_command_unsupported", command: kind }; } destroy() { if (this.pointer && typeof this.core?._free === "function") this.core._free(this.pointer); this.pointer = 0; } }