import { fnv1a64 } from "./replay.mjs"; const HOME_LIMIT_SCHEMA = "sim_home_limit_state.v1"; const KIN_TYPE_TRIVKINS = 1; const KIN_TYPE_XYZAC_TRT = 2; const KIN_CONFIG_SIZE = 1608; const KIN_SIZE = 1760; const HOME_LIMIT_SIZE = 192; const HOME_STATE_SIZE = 184; const HOME_STATE_OFFSET = 8; const CONFIG_OFFSETS = Object.freeze({ type: 0, mode: 4, joint_count: 8, kinematics_joint_count: 12, extra_joint_count: 16, coordinates: 20, x_rot_point: 40, y_offset: 72, z_offset: 80, tool_offset: 88, limit_enabled: 1448, joint_min: 1464, joint_max: 1536 }); const HOME_STATE_OFFSETS = Object.freeze({ pose: 0, joints: 72, joint_count: 144, homed: 148, home_sequence: 152, tick: 156, last_error: 160, limit_error: 164, kinematics_error: 168, homed_mask: 172 }); function call(core, name, returnType, argTypes, args) { const direct = core?.[`_${name}`] ?? core?.[name]; if (typeof direct === "function") return direct(...args); if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args); throw new Error(`missing core export: ${name}`); } 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 !== HOME_STATE_SIZE * 2 || !/^[0-9a-f]+$/i.test(value)) throw new Error("wasm_home_state_bytes_invalid"); const bytes = new Uint8Array(HOME_STATE_SIZE); for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16); return bytes; } function pose(heap, offset) { const doubles = heap.HEAPF64.subarray(offset / 8, offset / 8 + 9); return { x: doubles[0], y: doubles[1], z: doubles[2], a: doubles[3], b: doubles[4], c: doubles[5], u: doubles[6], v: doubles[7], w: doubles[8] }; } function finitePose(value) { return value && [value.x, value.y, value.z, value.a, value.b, value.c, value.u, value.v, value.w].every((entry) => typeof entry === "number" && Number.isFinite(entry)); } export class WasmHomeLimit { constructor(core, { jointCount = 3, limits = {}, kinematicsType = KIN_TYPE_TRIVKINS, coordinates = "XYZ", offsets = {} } = {}) { this.core = core; this.available = ["sim_kinematics_init", "sim_kinematics_home_limit_init", "sim_kinematics_home_limit_reset", "sim_kinematics_home_limit_home", "sim_kinematics_home_limit_home_joint", "sim_kinematics_home_limit_check", "sim_kinematics_home_limit_read"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function"); this.jointCount = jointCount; this.kinematicsType = kinematicsType; this.coordinates = String(coordinates).toUpperCase(); this.offsets = { ...offsets }; this.config = this.kinematics = this.homeLimit = this.state = this.joints = 0; this.stateBuffer = 0; this.limits = { x: [-500, 500], y: [-100, 100], z: [-240, 0.001], ...limits }; if (!this.available || typeof core?._malloc !== "function") return; this.config = core._malloc(KIN_CONFIG_SIZE); this.kinematics = core._malloc(KIN_SIZE); this.homeLimit = core._malloc(HOME_LIMIT_SIZE); this.stateBuffer = core._malloc(HOME_STATE_SIZE); this.joints = core._malloc(9 * 8); if (![this.config, this.kinematics, this.homeLimit, this.stateBuffer, this.joints].every(Boolean)) throw new Error("wasm_home_limit_allocation_failed"); this.core.HEAPU8.fill(0, this.config, this.config + KIN_CONFIG_SIZE); this.core.HEAPU8.fill(0, this.kinematics, this.kinematics + KIN_SIZE); this.core.HEAPU8.fill(0, this.homeLimit, this.homeLimit + HOME_LIMIT_SIZE); this.core.HEAPU8.fill(0, this.stateBuffer, this.stateBuffer + HOME_STATE_SIZE); this.core.HEAPU8.fill(0, this.joints, this.joints + 9 * 8); const configView = new DataView(this.core.HEAPU8.buffer, this.config, KIN_CONFIG_SIZE); configView.setInt32(CONFIG_OFFSETS.type, kinematicsType, true); configView.setInt32(CONFIG_OFFSETS.mode, 0, true); configView.setUint32(CONFIG_OFFSETS.joint_count, jointCount, true); configView.setUint32(CONFIG_OFFSETS.kinematics_joint_count, jointCount, true); configView.setUint32(CONFIG_OFFSETS.extra_joint_count, 0, true); for (const [index, axis] of [...this.coordinates].entries()) this.core.HEAPU8[this.config + CONFIG_OFFSETS.coordinates + index] = axis.charCodeAt(0); for (const [index, axis] of [...this.coordinates].entries()) { const lowerAxis = axis.toLowerCase(); const limit = this.limits[lowerAxis] ?? [-1e9, 1e9]; this.core.HEAPU8[this.config + CONFIG_OFFSETS.limit_enabled + index] = 1; configView.setFloat64(CONFIG_OFFSETS.joint_min + index * 8, limit[0], true); configView.setFloat64(CONFIG_OFFSETS.joint_max + index * 8, limit[1], true); } for (const [key, offset] of Object.entries({ x_rot_point: CONFIG_OFFSETS.x_rot_point, y_offset: CONFIG_OFFSETS.y_offset, z_offset: CONFIG_OFFSETS.z_offset, tool_offset: CONFIG_OFFSETS.tool_offset })) { configView.setFloat64(offset, Number(this.offsets[key] ?? 0), true); } const initialized = call(core, "sim_kinematics_init", "number", ["number", "number"], [this.kinematics, this.config]); if (initialized !== 0) throw new Error(`wasm_home_kinematics_init_${initialized}`); const homeInitialized = call(core, "sim_kinematics_home_limit_init", "number", ["number", "number", "number"], [this.homeLimit, this.kinematics, jointCount]); if (homeInitialized !== 0) throw new Error(`wasm_home_limit_init_${homeInitialized}`); } read() { if (!this.available) return { status: "blocked", state: { schema: HOME_LIMIT_SCHEMA, source: "wasm_home_limit_unavailable", homed: false, home_sequence: 0, tick: 0, joints: [0, 0, 0], pose: { x: 0, y: 0, z: 0 }, last_error: -840, limit_error: 0, kinematics_error: 0 } }; const result = call(this.core, "sim_kinematics_home_limit_read", "number", ["number", "number"], [this.homeLimit, this.stateBuffer]); if (result !== 0) return { status: "error", code: `wasm_home_limit_read_${result}` }; const ints = this.core.HEAPU32; const signed = this.core.HEAP32; const joints = [...this.core.HEAPF64.subarray(this.stateBuffer / 8 + 9, this.stateBuffer / 8 + 9 + this.jointCount)]; return { status: "verified", state: { schema: HOME_LIMIT_SCHEMA, source: "wasm_home_limit", pose: pose(this.core, this.stateBuffer), joints, joint_count: ints[this.stateBuffer / 4 + 36], homed: Boolean(ints[this.stateBuffer / 4 + 37]), home_sequence: ints[this.stateBuffer / 4 + 38], tick: ints[this.stateBuffer / 4 + 39], last_error: signed[this.stateBuffer / 4 + 40], limit_error: signed[this.stateBuffer / 4 + 41], kinematics_error: signed[this.stateBuffer / 4 + 42], homed_mask: ints[this.stateBuffer / 4 + 43] } }; } home(sequence = 0, axis = null) { if (!this.available) return this.read(); const index = axis === null || axis === undefined ? -1 : Number.isInteger(axis) ? axis : [...this.coordinates].findIndex((value) => value.toLowerCase() === String(axis).toLowerCase()); if (index >= this.jointCount) return { status: "error", code: "wasm_home_joint_invalid", state: this.read().state }; if (axis !== null && index < 0) return { status: "error", code: "wasm_home_axis_invalid", state: this.read().state }; const result = index < 0 ? call(this.core, "sim_kinematics_home_limit_home", "number", ["number", "number"], [this.homeLimit, sequence]) : call(this.core, "sim_kinematics_home_limit_home_joint", "number", ["number", "number", "number"], [this.homeLimit, sequence, index]); if (result !== 0) return { status: "error", code: `wasm_home_${result}`, state: this.read().state }; return this.read(); } check(joints) { if (!this.available) return this.read(); if (!Array.isArray(joints) || joints.length < this.jointCount || joints.some((value) => typeof value !== "number" || !Number.isFinite(value))) return { status: "error", code: "wasm_home_joints_invalid" }; this.core.HEAPF64.set(joints.slice(0, this.jointCount), this.joints / 8); const result = call(this.core, "sim_kinematics_home_limit_check", "number", ["number", "number", "number"], [this.homeLimit, this.joints, this.jointCount]); return result === 0 ? this.read() : { status: "error", code: `wasm_home_check_${result}`, state: this.read().state }; } reset() { if (!this.available) return this.read(); const result = call(this.core, "sim_kinematics_home_limit_reset", "number", ["number"], [this.homeLimit]); if (result !== 0) throw new Error(`wasm_home_limit_reset_${result}`); return this.read(); } exportState() { const current = this.read(); const stateBytes = this.available ? bytesToHex(this.core.HEAPU8.slice(this.homeLimit + HOME_STATE_OFFSET, this.homeLimit + HOME_STATE_OFFSET + HOME_STATE_SIZE)) : null; const unsigned = { schema: HOME_LIMIT_SCHEMA, available: this.available, joint_count: this.jointCount, state: current.state, state_bytes: stateBytes }; return { ...unsigned, state_hash: fnv1a64(unsigned) }; } restoreState(value) { if (!value || value.schema !== HOME_LIMIT_SCHEMA) throw new Error("wasm_home_restore_schema"); const unsigned = structuredClone(value); delete unsigned.state_hash; if (value.state_hash !== fnv1a64(unsigned)) throw new Error("wasm_home_restore_hash"); if (!this.available || value.available !== true || value.joint_count !== this.jointCount) throw new Error("wasm_home_restore_unavailable"); const bytes = hexToBytes(value.state_bytes); this.core.HEAPU8.set(bytes, this.homeLimit + HOME_STATE_OFFSET); const actual = this.read(); if (actual.status !== "verified" || fnv1a64(actual.state) !== fnv1a64(value.state)) throw new Error("wasm_home_restore_state_mismatch"); return this.exportState(); } destroy() { for (const pointer of [this.config, this.kinematics, this.homeLimit, this.stateBuffer, this.joints]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer); this.config = this.kinematics = this.homeLimit = this.stateBuffer = this.joints = 0; } } export { HOME_LIMIT_SCHEMA };