Fix web homing motion and synchronize preview state

This commit is contained in:
2026-09-09 09:31:32 +08:00
parent bade560372
commit eb5c58cdb3
11 changed files with 375 additions and 33 deletions

View File

@@ -33,6 +33,7 @@ import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
const XYZAC_AXIS_TO_JOINT = Object.freeze({ x: 0, y: 1, z: 2, a: 3, c: 4 });
const OPERATION_KINDS = new Set([
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
@@ -135,6 +136,12 @@ export class SimSession {
this.xyzacDeviceIo = null;
this.xyzacProgram = null;
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
// The Web panel presents fixed XYZAC world-axis controls. Homing still
// follows AXIS's KINEMATICS_BOTH transaction and uses a numeric joint;
// keep the presentation mode separate so the adapter can restore it
// after the accepted home transaction.
this.xyzacMotionMode = "TELEOP";
this.pendingHome = null;
this.xyzacRapidVelocity = 35;
this.xyzacMotionCycleTime = 0.01;
this.xyzacTaskCycleTicks = 1;
@@ -301,6 +308,7 @@ export class SimSession {
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
on_any_limit: Boolean(home.state?.limit_error),
following_error: 0,
motion_mode: this.xyzacProgram ? this.xyzacMotionMode : (home.state?.homed ? "TELEOP" : "FREE"),
jog: null,
stepping: this.wasmMotion.isStepping?.() ?? false,
estop: Boolean(ioState?.estop),
@@ -393,7 +401,7 @@ export class SimSession {
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
const ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state,
taskfile: Boolean(this.xyzacProgram?.path), queued_mdi_commands: task.queued_mdi_commands ?? 0,
motion_mode: homeLimit.state?.homed ? "TELEOP" : "FREE", kinematics_type: "BOTH",
motion_mode: this.xyzacProgram ? this.xyzacMotionMode : (homeLimit.state?.homed ? "TELEOP" : "FREE"), kinematics_type: "BOTH",
spindle_direction: Number(devices.spindles?.[0]?.direction ?? 0), tool: Number(devices.tool?.current ?? 0),
on_any_limit: motion.on_any_limit, paused: axis.operation_state === "PAUSED" });
const gates = this.xyzacProgram
@@ -449,6 +457,10 @@ export class SimSession {
schema: "axis-visual.v1",
pose: { ...position },
joints: Array.isArray(joints) ? [...joints] : [],
// Home changes the canonical current pose even when the simulation
// tick does not advance. The renderer uses this monotonic source
// sequence to reset its displayed pose without clearing Live Plot.
pose_epoch: Number(homeLimit.state?.home_sequence ?? 0),
live_path_epoch: this.livePathEpoch,
...(includeToolpath ? { toolpath: (preview?.segments ?? []).map((segment, index) => ({ id: index + 1, ...segment })) } : {}),
active_segment_id: wasmMotion.status === "verified" ? wasmMotion.state.active_id : authoritative.planner.active_id,
@@ -551,6 +563,9 @@ export class SimSession {
return { action: input.payload?.type === 0 ? "switchkinsIdentity" : input.payload?.type === 1 ? "switchkinsXyzac" : "switchkinsUserk", enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
}
const homeLimit = this.wasmHomeLimit.read();
if (this.pendingHome && ["HOME", "JOG_START", "MDI_EXECUTE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_STEP", "KINEMATICS_SELECT", "SET_MODE", "TOUCH_OFF", "SPINDLE_COMMAND", "SPINDLE_CONSTANT", "SPINDLE_BRAKE", "COOLANT_COMMAND"].includes(input.kind)) {
return { action: PANEL_GATE_ACTIONS[input.kind] ?? "home", enabled: false, reason: "xyzac_home_in_progress", taskState: task.state, interpState: "READING" };
}
const estop = Boolean(io.estop);
const ready = Boolean(io.ready) && !estop;
const loaded = program.status !== 0;
@@ -1372,6 +1387,8 @@ export class SimSession {
this.xyzacAppendDeferredOnActive = false;
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
this.pendingHome = null;
this.xyzacMotionMode = "TELEOP";
const resetProgram = this.resetXyzacProgramToLoaded();
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
this.task.setProgram("loaded", 0);
@@ -1475,6 +1492,8 @@ export class SimSession {
this.xyzacAppendDeferredOnActive = false;
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
this.pendingHome = null;
this.xyzacMotionMode = "TELEOP";
const resetProgram = this.resetXyzacProgramToLoaded();
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
this.task.setProgram("loaded", 0);
@@ -1594,6 +1613,8 @@ export class SimSession {
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
this.xyzacProgram = new WasmProgramRuntime(this.core);
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
this.xyzacMotionMode = "TELEOP";
this.pendingHome = null;
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
@@ -1941,6 +1962,18 @@ export class SimSession {
}
}
}
if (this.pendingHome && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) === 0) {
const pendingHome = this.pendingHome;
const home = this.wasmHomeLimit.home(pendingHome.seq, pendingHome.all ? null : pendingHome.joint);
if (home.status !== "verified") throw new Error(home.code ?? "wasm_home_commit_failed");
if (this.xyzacProgram && typeof this.xyzacProgram.synchronizePosition === "function") {
const synchronized = this.xyzacProgram.synchronizePosition(home.state.pose);
if (synchronized.status !== "verified") throw new Error(synchronized.code ?? "home_position_sync_failed");
}
this.xyzacMotionMode = pendingHome.modeBefore;
this.task.setMode("MANUAL");
this.pendingHome = null;
}
if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
if (this.motionAuthority === "wasm") {
if (wasmMotion.status === "verified") {
@@ -2392,17 +2425,54 @@ export class SimSession {
case "HOME": {
if (this.xyzacProgram) {
if (this.wasmHomeLimit.read().status !== "verified") return finish(this.error(input, "home_limit", "wasm_home_limit_unavailable"), "failed");
const requestedAxis = input.payload?.all ? null : String(input.payload?.axis ?? "").toLowerCase() || null;
if (requestedAxis) return finish(this.error(input, "home_limit", "home_joint_requires_joint_mode"), "failed");
const requestedJoint = input.payload?.joint;
if (requestedJoint !== undefined && (!Number.isInteger(requestedJoint) || requestedJoint < 0 || requestedJoint >= 5)) return finish(this.error(input, "home_limit", "home_joint_invalid"), "failed");
const payload = input.payload ?? {};
// Older callers used HOME with an empty payload for Home All. Keep
// that compatible while making the explicit Web contract
// HOME {all:true}.
const all = payload.all === true || (payload.all === undefined && payload.axis === undefined && payload.joint === undefined);
const requestedAxis = all ? null : String(payload.axis ?? "").trim().toLowerCase() || null;
let requestedJoint = payload.joint;
if (requestedAxis !== null) {
const mappedJoint = XYZAC_AXIS_TO_JOINT[requestedAxis];
if (mappedJoint === undefined) return finish(this.error(input, "home_limit", "home_axis_invalid"), "failed");
if (requestedJoint !== undefined && requestedJoint !== mappedJoint) return finish(this.error(input, "home_limit", "home_axis_joint_mismatch"), "failed");
requestedJoint = mappedJoint;
}
if (!all && requestedJoint === undefined) return finish(this.error(input, "home_limit", "home_joint_required"), "failed");
if (!all && (!Number.isInteger(requestedJoint) || requestedJoint < 0 || requestedJoint >= 5)) return finish(this.error(input, "home_limit", "home_joint_invalid"), "failed");
// AXIS home_all_joints()/home_joint() calls ensure_mode(MANUAL)
// before issuing the homing command.
// before go_home() selects FREE/joint and issues c.home().
try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
const home = this.wasmHomeLimit.home(input.seq, input.payload?.all ? null : requestedJoint ?? null);
if (home.status === "error") return finish(this.error(input, "home_limit", home.code), "failed");
const modeBefore = this.xyzacMotionMode;
const currentMotion = this.wasmMotion.read();
const currentPose = currentMotion.state?.position;
const currentJoints = Array.isArray(currentMotion.state?.joint_commanded) && currentMotion.state.joint_commanded.length === 5
? [...currentMotion.state.joint_commanded]
: [...(this.xyzacKinematics?.snapshot()?.joints ?? [currentPose.x, currentPose.y, currentPose.z, currentPose.a ?? 0, currentPose.c ?? 0])];
const targetHome = this.wasmHomeLimit.previewHome(all ? null : requestedJoint);
if (targetHome.status !== "verified") return finish(this.error(input, "home_limit", targetHome.code ?? "wasm_home_target_failed"), "failed");
const targetPose = targetHome.state.pose;
const needsMove = ["x", "y", "z", "a", "b", "c", "u", "v", "w"].some((axis) => Math.abs(Number(currentPose?.[axis] ?? 0) - Number(targetPose?.[axis] ?? 0)) > 1e-9);
this.xyzacMotionMode = "FREE";
if (needsMove) {
const enqueue = this.wasmMotion.enqueue({
start: currentPose,
end: targetPose,
velocity: Math.max(Number(this.xyzacRapidVelocity ?? 35), Number.EPSILON),
acceleration: Math.max(Number(this.xyzacAxisAccelerationLimits?.x ?? 300), Number.EPSILON),
motion_type: 1,
source_line: 0,
kinematics_mode: this.xyzacProgramKinematicsMode,
});
if (enqueue.status !== "verified") { this.xyzacMotionMode = modeBefore; return finish(this.error(input, "home_limit", enqueue.code ?? "home_motion_enqueue_failed"), "failed"); }
this.pendingHome = { seq: input.seq, all, axis: requestedAxis, joint: all ? null : requestedJoint, modeBefore, currentJoints, targetPose };
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, homing: { phase: "moving", target: all ? "all" : "joint", source_axis: requestedAxis, joint: all ? null : requestedJoint, motion_mode_before: modeBefore, motion_mode_during: "FREE", motion_mode_after: "FREE", target_pose: targetPose }, motion: this.wasmMotionProjection(), wasm_home_limit: this.wasmHomeLimit.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
}
const home = this.wasmHomeLimit.home(input.seq, all ? null : requestedJoint);
if (home.status !== "verified") { this.xyzacMotionMode = modeBefore; return finish(this.error(input, "home_limit", home.code), "failed"); }
this.xyzacMotionMode = modeBefore;
this.task.setMode("MANUAL");
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), wasm_home_limit: this.wasmHomeLimit.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, homing: { phase: "complete", target: all ? "all" : "joint", source_axis: requestedAxis, joint: all ? null : requestedJoint, motion_mode_before: modeBefore, motion_mode_during: "FREE", motion_mode_after: this.xyzacMotionMode }, motion: this.wasmMotionProjection(), wasm_home_limit: home, motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
}
const result = this.motion.home(input.payload?.axis ?? null);
if (result.status === "verified" && this.motionAuthority === "wasm") {
@@ -2450,13 +2520,28 @@ export class SimSession {
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
// The Web jog buttons are world-axis controls. Mirror AXIS's
// teleop selection for this adapter before submitting the TP jog.
this.xyzacMotionMode = "TELEOP";
this.wasmMotion.setPaused(false);
const requestedIncrement = input.payload?.increment;
const continuous = requestedIncrement === null || input.payload?.continuous === true;
const displayScale = ["x", "y", "z"].includes(axis) && input.payload?.units === "inch" ? 25.4 : 1;
const distance = continuous ? 0 : Number(requestedIncrement ?? 1) * displayScale;
if (!continuous && (!Number.isFinite(distance) || distance <= 0)) return finish(this.error(input, "motion_jog", "motion_jog_nonfinite"), "failed");
const jog = this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, c: 5 }[axis], direction, distance, input.seq);
// The selected XYZAC case uses the LinuxCNC TP jog ABI. Keep the
// existing queue-backed WASM cases working as well: they have no
// native jog entry point, so represent one incremental/continuous
// sample as a normal queued line and retain the same JOG_STOP abort
// path. This compatibility branch is never used by XYZAC TRT.
const jog = typeof this.wasmMotion.jog === "function"
? this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, c: 5 }[axis], direction, distance, input.seq)
: (() => {
const current = this.wasmMotion.read();
const start = current.state?.position ?? {};
const end = { ...start, [axis]: Number(start[axis] ?? 0) + direction * (continuous ? Math.max(Number(input.payload?.speed ?? 1) * this.xyzacMotionCycleTime * 5, Number.EPSILON) : distance) };
return this.wasmMotion.enqueue({ start, end, velocity: Math.max(Number(input.payload?.speed ?? 1), Number.EPSILON), acceleration: 1, motion_type: 1, source_line: input.seq });
})();
if (jog.status !== "verified") {
const code = jog.code === "linuxcnc_tp_jog_-1217" ? "motion_limit_range" : jog.code === "linuxcnc_tp_jog_-1215" ? "motion_jog_nonfinite" : jog.code;
return finish(this.error(input, "motion_queue", code), "failed");