Use native LinuxCNC homing and consolidate web panel status

This commit is contained in:
2026-09-09 12:28:02 +08:00
parent eb5c58cdb3
commit 0be2ee8ce1
35 changed files with 881 additions and 350 deletions

View File

@@ -25,6 +25,7 @@ import { axisSourceUi } from "./axis-source-ui.mjs";
import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
import { WasmLinuxcncTp } from "./wasm-linuxcnc-tp.mjs";
import { WasmHomeLimit } from "./wasm-home-limit.mjs";
import { WasmNativeHoming } from "./wasm-native-homing.mjs";
import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs";
import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
@@ -33,7 +34,6 @@ 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,13 +135,8 @@ export class SimSession {
this.xyzacIo = null;
this.xyzacDeviceIo = null;
this.xyzacProgram = null;
this.homingHistory = [];
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;
@@ -192,7 +187,7 @@ export class SimSession {
// Compact payloads are only valid for successful motion samples. Error
// responses must retain their code/reason instead of being rewritten into
// a delta with undefined fields.
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion;
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion && !statePayload.homing_sample && !this.wasmHomeLimit.active;
// The frontend retains the canonical preview from PROGRAM_LOAD. Repeating
// thousands of immutable segments on unrelated control responses makes a
// loaded program delay abort/power/home commands behind structured-clone
@@ -263,6 +258,13 @@ export class SimSession {
simulation_tick: tick,
payload: {
...responsePayload,
...(motionDelta ? { source_status: {
task: { mode: task.mode, state: axisState({ task, program, io: this.xyzacIo.export() }).source_task_state,
interpState: axisState({ task, program, io: this.xyzacIo.export() }).source_interp_state },
traj: { mode: this.wasmHomeLimit.mode, motion_type: currentMotion.state.motion_type },
homing_active: this.wasmHomeLimit.active, joint: this.wasmHomeLimit.read().state.joint,
homing_history: this.homingHistory,
} } : {}),
...(panelSnapshot ? { panel_snapshot: panelSnapshot } : {}),
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
task_authority: responsePayload.task_authority ?? this.task.authority(),
@@ -275,6 +277,7 @@ export class SimSession {
wasmMotionProjection(program = null) {
const wasmMotion = this.wasmMotion.read();
const position = wasmMotion.status === "verified" ? wasmMotion.state.position : { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 };
if (this.xyzacProgram && !this.wasmHomeLimit.active) this.wasmHomeLimit.sync(wasmMotion.state.joint_commanded);
const home = this.wasmHomeLimit.read();
const io = this.xyzacIo?.canonical?.();
const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null;
@@ -308,7 +311,8 @@ 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"),
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (home.state?.homed ? "TELEOP" : "FREE"),
homing: Boolean(home.state?.homing),
jog: null,
stepping: this.wasmMotion.isStepping?.() ?? false,
estop: Boolean(ioState?.estop),
@@ -401,9 +405,12 @@ 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: this.xyzacProgram ? this.xyzacMotionMode : (homeLimit.state?.homed ? "TELEOP" : "FREE"), kinematics_type: "BOTH",
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (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" });
if (homeLimit.state?.homing) {
for (const name of ["run", "step", "load", "reload", "home", "jog", "mdi", "touchOff"]) ui.gates[name] = { action: name, enabled: false, reason: "linuxcnc_homing_active" };
}
const gates = this.xyzacProgram
? {
load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }),
@@ -483,6 +490,14 @@ export class SimSession {
task,
program,
motion,
...(this.xyzacProgram ? { source_status: {
task: { mode: task.mode, state: axis.source_task_state, interpState: axis.source_interp_state },
traj: { mode: this.wasmHomeLimit.mode, motion_type: wasmMotion.state.motion_type },
homing_active: Boolean(homeLimit.state.homing),
homing_history: this.homingHistory,
joint: homeLimit.state.joint,
authority: { task: "wasm-task-adapter", homing: "linuxcnc-homing.c", home_state: "joint.N.home-state" },
} } : {}),
coordinates: { machine: [...axisCoordinates], relative: axisCoordinates.map((value, index) => value - displayedOffsets[index]), commanded: [...axisCoordinates], offsets: displayedOffsets, system: coordinateContext?.system ?? 1 },
overrides: { feed: motion.feed_override, spindle: [motion.spindle_override] },
wasm_motion: wasmMotion,
@@ -556,6 +571,8 @@ export class SimSession {
const program = this.xyzacProgram.snapshot();
const io = this.xyzacIo?.canonical?.() ?? {};
const wasmMotion = this.wasmMotion.read();
if (["HOME", "SET_MODE"].includes(input.kind) && Number(wasmMotion.state?.queue_depth) > 0) return { enabled: false, reason: "xyzac_motion_active" };
if (input.kind === "JOG_START" && !this.wasmHomeLimit.read().state.homed) return { enabled: false, reason: "xyzac_not_all_homed" };
if (input.kind === "PROGRAM_LOAD" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) {
return { action: null, enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
}
@@ -563,7 +580,7 @@ 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)) {
if (this.wasmHomeLimit.active && ["HOME", "JOG_START", "JOG_STOP", "MDI_EXECUTE", "PROGRAM_LOAD", "LOAD_WORKSPACE", "PROGRAM_RUN", "PROGRAM_STEP", "PROGRAM_RESTART", "PROGRAM_SEEK", "KINEMATICS_SELECT", "SET_MODE", "TOUCH_OFF", "RECOVER", "CHECKPOINT"].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);
@@ -659,8 +676,10 @@ export class SimSession {
// canonical P-number form (G54=P1, ..., G59.3=P9).
const block = `G10 L20 P${systemNumber} ${axis}${formatted}`;
const savedMode = this.task.state.mode;
const savedMotionMode = this.wasmHomeLimit.mode;
try {
if (savedMode !== "MDI") this.task.setMode("MDI");
this.wasmHomeLimit.setMode("COORD");
this.wasmMotion.setPaused(false);
const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position);
if (synchronized.status !== "verified") return synchronized;
@@ -675,6 +694,7 @@ export class SimSession {
return { status: "error", code: String(error?.message ?? error) };
} finally {
if (this.task.state.mode !== savedMode) this.task.setMode(savedMode);
this.wasmHomeLimit.setMode(savedMotionMode);
}
}
@@ -1359,7 +1379,7 @@ export class SimSession {
const deviceResponse = (kind, payload) => {
if (this.xyzacProgram && ["SPINDLE_COMMAND", "SPINDLE_BRAKE", "SPINDLE_CONSTANT"].includes(kind) && !payload?.source_keyboard && (kind !== "SPINDLE_COMMAND" || payload?.delta === undefined)) {
try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); }
try { if (this.task.state.mode !== "MANUAL") { this.task.setMode("MANUAL"); this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE"); } }
catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
}
const result = this.xyzacIo ? this.xyzacIo.command(kind, payload ?? {}) : this.devices.command(kind, payload ?? {});
@@ -1387,8 +1407,7 @@ export class SimSession {
this.xyzacAppendDeferredOnActive = false;
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
this.pendingHome = null;
this.xyzacMotionMode = "TELEOP";
this.wasmHomeLimit.cancel(true);
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);
@@ -1460,7 +1479,11 @@ export class SimSession {
}
const mode = String(input.payload?.mode ?? "").toUpperCase();
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
try {
this.task.setMode(mode);
if (this.xyzacProgram && mode === "MANUAL") this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE");
if (this.xyzacProgram && mode !== "MANUAL" && this.wasmHomeLimit.read().state.homed) this.wasmHomeLimit.setMode("COORD");
} catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
}
case "POWER": {
@@ -1492,8 +1515,7 @@ export class SimSession {
this.xyzacAppendDeferredOnActive = false;
this.wasmMotion.abort();
this.wasmMotion.setPaused(true);
this.pendingHome = null;
this.xyzacMotionMode = "TELEOP";
this.wasmHomeLimit.cancel(true);
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);
@@ -1612,9 +1634,8 @@ export class SimSession {
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
this.xyzacProgram = new WasmProgramRuntime(this.core);
this.homingHistory = [];
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);
@@ -1627,7 +1648,7 @@ export class SimSession {
}
if (configuration?.home_limits && this.motionAuthority === "wasm") {
this.wasmHomeLimit.destroy();
this.wasmHomeLimit = new WasmHomeLimit(this.core, {
this.wasmHomeLimit = this.xyzacProgram ? new WasmNativeHoming(this.core, configuration, this.xyzacKinematics) : new WasmHomeLimit(this.core, {
jointCount: configuration.home_limits.joint_count ?? 3,
kinematicsType: configuration.home_limits.kinematics_type ?? 1,
coordinates: configuration.home_limits.coordinates ?? "XYZ",
@@ -1681,6 +1702,7 @@ export class SimSession {
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
}
case "SIM_STEP": {
const homingSample = Boolean(this.xyzacProgram && this.wasmHomeLimit.active);
const cycles = input.payload?.cycles;
const limit = validateLimit("max_tick_step", cycles);
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
@@ -1711,6 +1733,7 @@ export class SimSession {
this.scheduler.step(cycles, {
task: (nextTick) => {
if (this.xyzacProgram) {
if (this.wasmHomeLimit.active) return;
const wasmState = this.xyzacProgram.snapshot();
if (wasmState.status !== 2) return;
if (this.pendingTaskSync !== 0) {
@@ -1907,7 +1930,24 @@ export class SimSession {
let wasmMotion = this.wasmMotion.read();
const programPaused = this.xyzacProgram?.snapshot().status === 3;
for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) {
if (this.xyzacProgram && this.wasmHomeLimit.active) {
const home = this.wasmHomeLimit.step();
if (home.status !== "verified") throw new Error(home.code);
const transition = home.state.joint.map((j) => `${j.home_state}:${Number(j.homing)}:${Number(j.homed)}`).join(",");
if (this.homingHistory.at(-1)?.transition !== transition) this.homingHistory = [...this.homingHistory.slice(-63), { tick: home.state.tick, transition, joint: home.state.joint }];
// Coordinate synchronization after native motor-offset /
// free_tp updates, not an artificial move to machine zero.
this.wasmMotion.setActiveKinematicsTag(this.xyzacKinematics.activeMode);
this.wasmMotion.setPosition(home.state.pose);
wasmMotion = this.wasmMotion.read();
if (!home.state.homing) {
const sync = this.xyzacProgram.synchronizePosition(home.state.pose);
if (sync.status !== "verified") throw new Error(sync.code);
}
continue;
}
if (programPaused && !this.wasmMotion.isStepping()) break;
if (this.wasmMotion.controls.paused && this.wasmMotion.read().state.queue_depth === 0) continue;
if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") {
this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3);
}
@@ -1962,18 +2002,6 @@ 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") {
@@ -2051,7 +2079,7 @@ export class SimSession {
? null
: this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
return finish(this.envelope("COMMAND_RESULT", input, {
status: "verified", cycles, program_steps, execution_trace,
status: "verified", cycles, program_steps, execution_trace, homing_sample: homingSample,
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
motion_authority: this.motionAuthority,
task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative,
@@ -2143,7 +2171,7 @@ export class SimSession {
if (this.xyzacProgram) {
const previousProgramStatus = this.xyzacProgram.snapshot().program_status;
const result = this.xyzacProgram.start();
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); this.wasmHomeLimit.setMode("COORD"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, reopened_taskfile: ["completed", "aborted"].includes(previousProgramStatus), start_line: 0, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
const result = this.program.start();
@@ -2165,6 +2193,7 @@ export class SimSession {
}), result.status === "verified" ? "verified" : "failed");
}
case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": {
if (this.xyzacProgram && input.kind === "PROGRAM_ABORT" && this.wasmHomeLimit.active) this.wasmHomeLimit.cancel();
const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort";
if (this.xyzacProgram) {
// PAUSE/RESUME are interpreter and TP state transitions. They must
@@ -2275,6 +2304,7 @@ export class SimSession {
const current = this.xyzacProgram.snapshot();
const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state);
this.task.setMode("AUTO");
this.wasmHomeLimit.setMode("COORD");
if (idle) {
const started = this.xyzacProgram.start();
if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed");
@@ -2364,6 +2394,7 @@ export class SimSession {
const block = input.payload?.block ?? input.payload?.command;
if (this.xyzacProgram) {
this.task.setMode("MDI");
this.wasmHomeLimit.setMode("COORD");
// AXIS send_mdi_command() leaves the interpreter/TP runnable after
// a previous manual jog stop. JOG_STOP intentionally pauses the
// WASM TP, so clear that transport pause before queuing MDI motion;
@@ -2416,6 +2447,7 @@ export class SimSession {
const mode = Number(input.payload?.type);
if (![XYZAC_MODES.identity, XYZAC_MODES.xyzac, XYZAC_MODES.userk].includes(mode)) return finish(this.error(input, "kinematics", "xyzac_kinematics_type_invalid"), "failed");
this.task.setMode("MDI");
this.wasmHomeLimit.setMode("COORD");
const result = this.switchXyzacKinematics(mode);
const code = mode === XYZAC_MODES.xyzac ? "M428" : mode === XYZAC_MODES.identity ? "M429" : "M430";
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: result.kinematics ?? this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
@@ -2430,49 +2462,22 @@ export class SimSession {
// 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 && payload.joint !== undefined) return finish(this.error(input, "home_limit", "home_target_ambiguous"), "failed");
if (payload.axis !== undefined) return finish(this.error(input, "home_limit", "home_joint_requires_joint_mode"), "failed");
const requestedJoint = payload.joint;
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 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 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, 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");
try {
this.wasmHomeLimit.sync(this.wasmMotion.read().state.joint_commanded);
const home = this.wasmHomeLimit.home(input.seq, all ? null : requestedJoint);
this.homingHistory = [{ tick: home.state.tick, transition: "command", joint: home.state.joint }];
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind,
homing: { phase: "accepted", joint: all ? -1 : requestedJoint, source: "linuxcnc-homing.c" },
wasm_home_limit: home, motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
} catch (error) { return finish(this.error(input, "home_limit", error.message), "failed"); }
}
const result = this.motion.home(input.payload?.axis ?? null);
if (result.status === "verified" && this.motionAuthority === "wasm") {
@@ -2520,9 +2525,7 @@ 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";
if (this.xyzacProgram) this.wasmHomeLimit.setMode("TELEOP");
this.wasmMotion.setPaused(false);
const requestedIncrement = input.payload?.increment;
const continuous = requestedIncrement === null || input.payload?.continuous === true;