119 lines
7.0 KiB
JavaScript
119 lines
7.0 KiB
JavaScript
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; }
|
|
}
|