181 lines
11 KiB
JavaScript
181 lines
11 KiB
JavaScript
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_COMMAND") {
|
|
if (payload.delta !== undefined) return this.invoke("sim_io_spindle_adjust", [payload.spindle ?? 0, Number(payload.delta)]);
|
|
return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, payload.rpm ?? payload.speed ?? 0, 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_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 };
|