Enforce LinuxCNC WASM authority for XYZAC frontend

This commit is contained in:
2026-09-02 07:43:47 +08:00
parent c60e3a1e8f
commit 6b689fd69e
36 changed files with 514 additions and 210 deletions

View File

@@ -14,32 +14,10 @@ function call(core, name, returnType, argTypes, args) {
function poseArray(value = {}) { return ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => Number(value[axis] ?? 0)); }
function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis, index) => [axis, Number(values[index] ?? 0)])); }
function cubicState() { return { x0: 0, x1: 0, x2: 0, x3: 0, t: 0, need: true, coeff: [0, 0, 0, 0] }; }
function cubicAdd(state, point, segmentTime = 0.001) {
if (!state.need) return;
state.x0 = state.x1; state.x1 = state.x2; state.x2 = state.x3; state.x3 = Number(point);
const wp0 = (state.x0 + 4 * state.x1 + state.x2) / 6;
const wp1 = (state.x1 + 4 * state.x2 + state.x3) / 6;
const vel0 = (state.x2 - state.x0) / (2 * segmentTime);
const vel1 = (state.x3 - state.x1) / (2 * segmentTime);
const b = 3 * (wp1 - wp0) / (segmentTime * segmentTime) - (2 * vel0 + vel1) / segmentTime;
const a = (vel1 - vel0) / (3 * segmentTime * segmentTime) - 2 * b / (3 * segmentTime);
state.coeff = [a, b, vel0, wp0]; state.t = 0; state.need = false;
}
function cubicInterpolate(state, segmentTime = 0.001) {
if (state.need) cubicAdd(state, state.x3, segmentTime);
const [a, b, c, d] = state.coeff;
const t = state.t;
const value = ((a * t + b) * t + c) * t + d;
state.t += segmentTime;
if (Math.abs(segmentTime - state.t) < segmentTime * 0.5) state.need = true;
return value;
}
export class WasmLinuxcncTp {
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300 } = {}) {
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) {
this.core = core;
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
this.cycleTime = cycleTime;
this.velocityLimit = velocityLimit;
this.accelerationLimit = accelerationLimit;
@@ -49,7 +27,6 @@ export class WasmLinuxcncTp {
this.centerPointer = 0;
this.normalPointer = 0;
this.statePointer = 0;
this.history = [];
this.activeTag = 0;
this.segmentTags = new Map();
this.lastActiveId = 0;
@@ -60,9 +37,7 @@ export class WasmLinuxcncTp {
this.lastState = null;
this.queueDepthOverride = null;
this.kinematics = null;
this.cubic = Array.from({ length: 5 }, () => cubicState());
this.previousJointCommanded = [0, 0, 0, 0, 0];
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, paused: false };
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, velocity_scale: 1, jog_scale: 1, paused: false };
if (!this.available || typeof core?._malloc !== "function") return;
this.planner = core._malloc(call(core, "sim_linuxcnc_tp_size", "number", [], []));
this.posePointer = core._malloc(POSE_BYTES);
@@ -74,6 +49,10 @@ export class WasmLinuxcncTp {
if (result !== 0) throw new Error(`linuxcnc_tp_init_${result}`);
this.setPosition({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
this.setProfile(0);
for (const [axis, limit] of Object.entries(limits)) {
const index = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5, u: 6, v: 7, w: 8 }[axis];
if (index !== undefined && Array.isArray(limit) && limit.length === 2) this.setAxisLimit(index, Number(limit[0]), Number(limit[1]));
}
}
setPosition(value) {
@@ -111,6 +90,11 @@ export class WasmLinuxcncTp {
setLookahead(value) { if (!Number.isSafeInteger(value) || value < 1) throw new Error("linuxcnc_tp_lookahead_invalid"); this.controls.lookahead = value; return this.read(); }
setDynamics(accelerationLimit, jerkLimit) { this.accelerationLimit = Number(accelerationLimit); this.controls.acceleration_limit = this.accelerationLimit; this.controls.jerk_limit = Number(jerkLimit); if (this.available) call(this.core, "sim_linuxcnc_tp_set_dynamics", "number", ["number", "number"], [this.planner, this.accelerationLimit]); return this.read(); }
setFeedOverride(value) { this.controls.feed_override = Number(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_feed_override", "number", ["number", "number"], [this.planner, this.controls.feed_override]); return this.read(); }
setVelocityScale(value) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_velocity_scale", "number", ["number", "number"], [this.planner, Number(value)]); if (result !== 0) throw new Error(`linuxcnc_tp_velocity_scale_${result}`); return this.read(); }
setJogScale(value) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_jog_scale", "number", ["number", "number"], [this.planner, Number(value)]); if (result !== 0) throw new Error(`linuxcnc_tp_jog_scale_${result}`); return this.read(); }
setAxisLimit(axis, minimum, maximum) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_axis_limit", "number", ["number", "number", "number", "number"], [this.planner, Number(axis), Number(minimum), Number(maximum)]); if (result !== 0) throw new Error(`linuxcnc_tp_axis_limit_${result}`); return this.read(); }
jog(axis, direction, increment, tag = 0) { if (!this.available) return { status: "blocked", code: "linuxcnc_tp_unavailable" }; const result = call(this.core, "sim_linuxcnc_tp_jog", "number", ["number", "number", "number", "number", "number"], [this.planner, Number(axis), Number(direction), Number(increment), Number(tag)]); return result === 0 ? { status: "verified", command: "JOG_START" } : { status: "error", code: `linuxcnc_tp_jog_${result}` }; }
jogVelocity(axis) { if (!this.available) return 0; const index = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5, u: 6, v: 7, w: 8 }[String(axis).toLowerCase()]; return index === undefined ? 0 : Number(call(this.core, "sim_linuxcnc_tp_jog_velocity", "number", ["number", "number"], [this.planner, index])); }
setPaused(value) { this.controls.paused = Boolean(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_paused", "number", ["number", "number"], [this.planner, this.controls.paused ? 1 : 0]); return this.read(); }
enqueue(segment) {
@@ -158,7 +142,7 @@ export class WasmLinuxcncTp {
if (Number(activeTag) === 0) return [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
const inverse = this.kinematics?.inverseMode?.(Number(activeTag), position) ?? this.kinematics?.inverse?.(position);
if (inverse?.status === "verified" && Array.isArray(inverse.joints)) return inverse.joints.slice(0, 5);
return this.previousJointCommanded.slice();
throw new Error(`linuxcnc_tp_joint_inverse_${inverse?.code ?? "unavailable"}`);
}
step() {
@@ -169,13 +153,9 @@ export class WasmLinuxcncTp {
const state = this.read();
if (state.status === "verified") {
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
state.state.joint_commanded = commanded;
state.state.joint_actual = this.previousJointCommanded.slice();
this.previousJointCommanded = commanded.slice();
state.state.joint_commanded = targetJoints;
state.state.joint_actual = targetJoints.slice();
}
if (state.status === "verified") this.history.push(structuredClone(state.state));
return state;
}
@@ -186,18 +166,12 @@ export class WasmLinuxcncTp {
const state = this.read();
if (state.status === "verified") {
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
state.state.joint_commanded = commanded;
state.state.joint_actual = this.previousJointCommanded.slice();
this.previousJointCommanded = commanded.slice();
state.state.joint_commanded = targetJoints;
state.state.joint_actual = targetJoints.slice();
}
if (state.status === "verified") this.history.push(structuredClone(state.state));
return state;
}
drainTrace() { const trace = this.history; this.history = []; return trace; }
read() {
if (!this.available) return { status: "blocked", state: { position: toPose([]), velocity: 0, progress: 0, tick: 0, queue_depth: 0, active_id: 0, active_tag: 0, motion_type: 0, status: -1214, exec_state: this.executionState, source: "linuxcnc-source-tp" } };
const result = call(this.core, "sim_linuxcnc_tp_read_state", "number", ["number", "number"], [this.planner, this.statePointer]);
@@ -207,13 +181,22 @@ export class WasmLinuxcncTp {
const activeId = ints[2];
if (activeId && activeId !== this.lastActiveId && this.segmentTags.has(activeId)) this.activeTag = this.segmentTags.get(activeId);
this.lastActiveId = activeId;
const state = { position: toPose(doubles), velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: this.previousJointCommanded.slice(), joint_actual: this.previousJointCommanded.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, source: "linuxcnc-source-tp" };
const velocityScale = Number(call(this.core, "sim_linuxcnc_tp_velocity_scale", "number", ["number"], [this.planner]));
const velocityLimit = Number(call(this.core, "sim_linuxcnc_tp_velocity_limit", "number", ["number"], [this.planner]));
const jogScale = Number(call(this.core, "sim_linuxcnc_tp_jog_scale", "number", ["number"], [this.planner]));
const feedOverride = Number(call(this.core, "sim_linuxcnc_tp_feed_override", "number", ["number"], [this.planner]));
this.controls.velocity_scale = velocityScale;
this.controls.jog_scale = jogScale;
this.controls.feed_override = feedOverride;
const position = toPose(doubles);
const joints = this.commandedJoints(position, this.activeTag);
const state = { position, velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity("a"), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" };
this.lastState = state;
return { status: "verified", state };
}
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; this.cubic = Array.from({ length: 5 }, () => cubicState()); this.previousJointCommanded = [0, 0, 0, 0, 0]; return this.read(); }
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
exportState() {
const current = this.read();
const segments = [];
@@ -241,6 +224,8 @@ export class WasmLinuxcncTp {
this.executionState = Number(value.execution_state ?? 3);
this.setProfile(value.controls?.profile ?? 0, value.controls?.tolerance ?? 0);
this.setFeedOverride(value.controls?.feed_override ?? 1);
this.setVelocityScale(value.controls?.velocity_scale ?? 1);
this.setJogScale(value.controls?.jog_scale ?? 1);
this.setPosition(value.state?.position ?? {});
for (const segment of value.segments ?? []) this.enqueue({ ...segment, kinematics_mode: segment.kinematics_mode ?? value.active_tag ?? 0 });
if (this.available && value.state) {