Align AXIS control flow and fix pause and program rendering

Document source UI gates, explicit native live-plot clearing, and remaining parity boundaries. Fix pause/resume transport continuity, repeated stepping, coordinate previews, and spindle override state. Optimize large program snapshots and rendering, and cover the control flows with native Tcl, WASM, and browser checks.

Validation: 22 native tests and source/browser checks pass. Worker/App regression: 113 of 123 pass; 10 pre-existing failures remain documented with no new failures.
This commit is contained in:
2026-09-08 10:46:27 +08:00
parent 845075f4a3
commit bade560372
55 changed files with 2495 additions and 286 deletions

View File

@@ -21,6 +21,7 @@ import { createTrace, validateTrace } from "./trace-export.mjs";
import { ConfigurationBatchRuntime } from "../workspace/configuration-batch.mjs";
import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
import { axisState } from "./axis-state.mjs";
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";
@@ -301,6 +302,7 @@ export class SimSession {
on_any_limit: Boolean(home.state?.limit_error),
following_error: 0,
jog: null,
stepping: this.wasmMotion.isStepping?.() ?? false,
estop: Boolean(ioState?.estop),
fault: ioState?.fault ? (ioState.fault_reason ?? "io_fault") : null,
};
@@ -369,6 +371,11 @@ export class SimSession {
return { status: "verified" };
}
clearLivePlot() {
this.livePathEpoch += 1;
return this.livePathEpoch;
}
panelSnapshot({ task = this.task.export(), program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : null, input = {}, tick = this.tick, motionTrace = null, includeToolpath = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeConfiguration = input.kind !== "SIM_STEP" || !this.xyzacProgram, includeStableRuntime = input.kind !== "SIM_STEP" || !this.xyzacProgram } = {}) {
const motion = this.xyzacProgram ? this.wasmMotionProjection(program) : this.motion.export();
const wasmMotion = this.wasmMotion.read();
@@ -384,9 +391,15 @@ export class SimSession {
const xyzac = this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
const gateInput = { task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
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",
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
? {
load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }),
reload: this.xyzacProgram.path ? this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }) : { enabled: false, reason: "axis_taskfile_missing" },
run: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }),
runLine: this.xyzacCommandGate({ kind: "PROGRAM_RUN", payload: {} }),
step: this.xyzacCommandGate({ kind: "PROGRAM_STEP", payload: {} }),
@@ -442,8 +455,14 @@ export class SimSession {
active_progress: wasmMotion.status === "verified" ? wasmMotion.state.progress : 0,
...(Array.isArray(motionTrace) && motionTrace.length ? { motion_trace: motionTrace } : {}),
source: "wasm_canonical_motion",
velocity_mm_min: motion.velocity_mm_min ?? 0,
machine_limits: this.wasmMotion.axisLimits?.() ?? null,
coordinate_offset: program?.coordinate_context?.offsets?.[(program?.coordinate_context?.system ?? 1) - 1]?.slice(0, 3) ?? null,
};
const axisCoordinates = [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
const coordinateContext = program?.coordinate_context;
const activeOffsets = coordinateContext?.offsets?.[coordinateContext.system - 1] ?? Array(9).fill(0);
const displayedOffsets = [0, 1, 2, 3, 5].map((index) => activeOffsets[index]);
const unsigned = {
schema: "axis-panel-snapshot.v1",
context_id: this.contextId,
@@ -452,13 +471,14 @@ export class SimSession {
task,
program,
motion,
coordinates: { machine: [...axisCoordinates], relative: [...axisCoordinates], commanded: [...axisCoordinates] },
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,
...(includeStableRuntime ? { home_limit: homeLimit, hal: { configuration: this.halState, runtime: halRuntime }, devices, device_io: deviceIo } : {}),
...(includeConfiguration ? { configuration: this.configurationState } : {}),
axis_state: axis,
gates,
...(this.xyzacProgram ? { ui_gates: { ...ui.gates, toolTouchOff: gates.toolTouchOff }, ui_relief: ui.relief } : {}),
visual,
authoritative,
motion_authority: this.motionAuthority,
@@ -536,8 +556,8 @@ export class SimSession {
const loaded = program.status !== 0;
const action = PANEL_GATE_ACTIONS[input.kind] ?? null;
if (input.kind === "TOOL_TOUCH_OFF") return { action: "toolTouchOff", enabled: false, reason: "blocked_by_abi_tool_offsets", taskState: task.state, interpState: "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status } };
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" || input.kind === "SET_JOG_SCALE" ? WASM_GATE_COMMANDS.JOG
: input.kind === "SET_MAX_VELOCITY" || input.kind === "SET_SPINDLE_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" ? WASM_GATE_COMMANDS.JOG
: input.kind === "SET_JOG_SCALE" || input.kind === "SET_MAX_VELOCITY" || input.kind === "SET_SPINDLE_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
: input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
: input.kind === "SPINDLE_COMMAND" || input.kind === "SPINDLE_CONSTANT" || input.kind === "SPINDLE_BRAKE" ? WASM_GATE_COMMANDS.SPINDLE
: input.kind === "COOLANT_COMMAND" ? WASM_GATE_COMMANDS.COOLANT
@@ -607,15 +627,16 @@ export class SimSession {
const value = Number(payload.value);
if (!Number.isFinite(value)) return { status: "error", code: "touch_off_value_invalid" };
const selectedSystem = String(payload.system ?? "P1 G54").trim().toUpperCase();
const systemMatch = selectedSystem.match(/^(P[0-9]|G5[4-9](?:\.[123])?)(?:\s+.*)?$/);
const systemMatch = selectedSystem.match(/^(P[0-9]|G(?:5[4-8]|59(?:\.[123])?))(?:\s+.*)?$/);
if (!systemMatch) return { status: "error", code: "touch_off_system_invalid" };
const systemToken = systemMatch[1];
const systemNumber = systemToken.startsWith("P") ? Number(systemToken.slice(1)) : 1 + (Number(systemToken.slice(1, 3)) - 54) + (systemToken.includes(".") ? Number(systemToken.slice(-1)) : 0);
if (!Number.isInteger(systemNumber) || systemNumber < 0 || systemNumber > 9) return { status: "error", code: "touch_off_system_invalid" };
const displayUnits = String(payload.units ?? "").toLowerCase();
const programUnits = Number(this.xyzacProgram.snapshot().units);
const converted = displayUnits === "mm" && programUnits === 20 ? value / 25.4
: displayUnits === "inch" && programUnits === 21 ? value * 25.4 : value;
const linear = ["X", "Y", "Z"].includes(axis);
const converted = linear && displayUnits === "mm" && programUnits === 20 ? value / 25.4
: linear && displayUnits === "inch" && programUnits === 21 ? value * 25.4 : value;
const formatted = Number(converted.toFixed(12));
const sourceBlock = `G10 L20 ${systemToken} ${axis}${formatted}`;
// The LinuxCNC interpreter accepts the G54..G59 word in this command;
@@ -626,14 +647,19 @@ export class SimSession {
try {
if (savedMode !== "MDI") this.task.setMode("MDI");
this.wasmMotion.setPaused(false);
const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position);
if (synchronized.status !== "verified") return synchronized;
const offset = this.xyzacProgram.executeBlock(block);
if (offset.status !== "verified") return { ...offset, touch_off: { axis, value, system: selectedSystem, source_system: systemToken, source_block: sourceBlock, block } };
const preview = this.xyzacProgram.preview();
this.previewState = preview.status === "verified" ? preview : null;
if (preview.status !== "verified") return preview;
this.task.setProgram("loaded", this.xyzacProgram.snapshot().current_line);
if (savedMode !== "MDI") this.task.setMode(savedMode);
return { status: "verified", command: "TOUCH_OFF", source_block: sourceBlock, block, touch_off: { axis, value, system: selectedSystem, source_system: systemToken, source_block: sourceBlock, block }, program: this.xyzacProgram.snapshot(), motion: offset.motion ?? null };
} catch (error) {
try { if (savedMode !== "MDI" && this.task.state.mode !== savedMode) this.task.setMode(savedMode); } catch { /* preserve the original interpreter error */ }
return { status: "error", code: String(error?.message ?? error) };
} finally {
if (this.task.state.mode !== savedMode) this.task.setMode(savedMode);
}
}
@@ -1317,6 +1343,10 @@ 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"); }
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 ?? {});
if (result.status === "verified" && this.xyzacProgram && kind === "ESTOP" && payload?.active === true) {
// LinuxCNC emcTaskSetState(ESTOP) calls emcTaskAbort: clear the
@@ -1390,7 +1420,7 @@ export class SimSession {
const visualClearResponse = () => {
const before = { task: this.task.hash(), motion: this.wasmMotion.exportState().state_hash, program: this.xyzacProgram?.snapshot().state_hash ?? null };
this.livePathEpoch += 1;
this.clearLivePlot();
const after = { task: this.task.hash(), motion: this.wasmMotion.exportState().state_hash, program: this.xyzacProgram?.snapshot().state_hash ?? null };
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, visual_clear: true, live_path_epoch: this.livePathEpoch, visual_only: true, before, after, operation_id: input.operation_id ?? null }), "verified");
};
@@ -1637,7 +1667,7 @@ export class SimSession {
// worker. LinuxCNC keeps the paused task/TP state stable; accept that
// transport frame as a no-op instead of rejecting it at the protocol
// layer and leaving the next PROGRAM_RESUME with a sequence gap.
if (this.xyzacProgram && (this.task.state.state === "PAUSED" || this.xyzacProgram.snapshot().status === 3)) {
if (this.xyzacProgram && (this.task.state.state === "PAUSED" || this.xyzacProgram.snapshot().status === 3) && !this.wasmMotion.isStepping()) {
const pausedProgram = this.xyzacProgram.snapshot();
return finish(this.envelope("COMMAND_RESULT", input, {
status: "verified",
@@ -1661,7 +1691,7 @@ export class SimSession {
task: (nextTick) => {
if (this.xyzacProgram) {
const wasmState = this.xyzacProgram.snapshot();
if (![2, 3].includes(wasmState.status)) return;
if (wasmState.status !== 2) return;
if (this.pendingTaskSync !== 0) {
this.pendingTaskSync = 0;
return;
@@ -1854,7 +1884,9 @@ export class SimSession {
// cycle, so advance the WASM motion queue by the configured
// servo/task ratio while publishing one stable task snapshot.
let wasmMotion = this.wasmMotion.read();
const programPaused = this.xyzacProgram?.snapshot().status === 3;
for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) {
if (programPaused && !this.wasmMotion.isStepping()) break;
if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") {
this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3);
}
@@ -1971,6 +2003,7 @@ export class SimSession {
const status = this.xyzacProgram ? (program.status === 4 ? "completed" : program.status === 2 ? "running" : program.status === 3 ? "paused" : "loaded") : (program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status);
this.task.setProgram(status, this.xyzacProgram ? (program.execution_source_line ?? program.current_line) : program.line);
if (status === "completed" && this.task.state.state === "RUNNING" && (this.motionAuthority !== "wasm" || this.wasmMotion.read().state.queue_depth === 0)) this.task.transition("STOPPED");
if (this.xyzacProgram && this.task.state.mode === "MDI" && this.task.state.state === "RUNNING" && this.wasmMotion.read().state.queue_depth === 0) this.task.transition("STOPPED");
}
const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motionProjection(), plc: this.plc.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), xyzac_timing: { servo_cycle_time: this.xyzacMotionCycleTime, task_cycle_ticks: this.xyzacTaskCycleTicks } };
if (program) snapshotState.program = program;
@@ -2047,7 +2080,8 @@ export class SimSession {
}
restoreMode();
const finalStatus = result.status === "verified" && preview.status === "verified" ? "verified" : "error";
return finish(this.envelope(finalStatus === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, preview, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), finalStatus);
return finish(this.envelope(finalStatus === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, preview, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null,
}), finalStatus);
}
restoreMode();
const result = this.program.load(source, input.payload?.path ?? null);
@@ -2130,7 +2164,7 @@ export class SimSession {
// false ERR_STATE. The LinuxCNC WASM TP remains authoritative for
// the actual pause/resume motion transition.
if (action === "pause") { this.wasmMotion.setPaused(true); this.task.transition("PAUSED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("paused", program.execution_source_line ?? program.current_line); }
else if (action === "resume") { this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
else if (action === "resume") { this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram(program.status === 4 ? "completed" : program.status === 1 ? "loaded" : "running", program.execution_source_line ?? program.current_line); }
else { this.wasmMotion.abort(); this.task.transition("STOPPED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("aborted", program.execution_source_line ?? program.current_line); }
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, 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");
@@ -2204,25 +2238,47 @@ export class SimSession {
}
case "PROGRAM_STEP": {
if (this.xyzacProgram) {
const current = this.xyzacProgram.snapshot();
if (current.status === 1 && this.task.state.mode !== "AUTO") this.task.setMode("AUTO");
if (current.status === 1) this.xyzacProgram.start();
const result = this.xyzacProgram.step();
if (result.status === "verified") {
// AXIS AUTO_STEP returns to an interpreter-paused state after the
// single canonical block. Keep the WASM interpreter status and
// task status aligned so the Resume gate is authoritative.
if (result.program_status !== 4) {
const pauseResult = this.xyzacProgram.pause();
if (pauseResult.status !== "verified") return finish(this.error(input, "program_pause", pauseResult.code ?? "program_pause_failed"), "failed");
try {
const current = this.xyzacProgram.snapshot();
const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state);
this.task.setMode("AUTO");
if (idle) {
const started = this.xyzacProgram.start();
if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed");
} else if (current.status === 3 && this.wasmMotion.queueDepth() > 0) {
this.wasmMotion.stepLine();
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, program: current, task: this.task.export(), motion: this.wasmMotionProjection() }), "verified");
} else {
if (current.status === 3) this.xyzacProgram.resume();
const stepped = this.xyzacProgram.step();
if (stepped.status !== "verified") return finish(this.error(input, "program_step", stepped.code), "failed");
const sync = this.syncXyzacProgramSpindle(stepped.program);
if (sync.status !== "verified" && sync.status !== "skipped") return finish(this.error(input, "program_spindle", sync.code), "failed");
if (stepped.has_control && [428, 429, 430].includes(stepped.control_code)) {
const selected = this.switchXyzacKinematics({ 428: 1, 429: 0, 430: 2 }[stepped.control_code]);
if (selected.status !== "verified") return finish(this.error(input, "program_step", selected.code), "failed");
}
if (stepped.motion) {
stepped.motion.kinematics_mode = this.xyzacProgramKinematicsMode;
const queued = this.wasmMotion.enqueue(stepped.motion);
if (queued.status === "error") return finish(this.error(input, "motion_queue", queued.code), "failed");
}
}
if (this.task.state.state === "READY") this.task.transition("RUNNING");
if (result.motion) this.wasmMotion.enqueue(result.motion);
if (this.xyzacProgram.snapshot().status === 4 && this.wasmMotion.read().state.queue_depth === 0) {
this.task.setProgram("completed", this.xyzacProgram.snapshot().current_line);
if (this.task.state.state !== "STOPPED") this.task.transition("STOPPED");
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion: this.wasmMotionProjection() }), "verified");
}
const paused = this.xyzacProgram.pause();
if (paused.status !== "verified") return finish(this.error(input, "program_pause", paused.code), "failed");
this.wasmMotion.setPaused(true);
this.task.setProgram(result.program_status === 4 ? "completed" : "paused", result.line);
this.task.transition(result.program_status === 4 ? "STOPPED" : "PAUSED");
if (["READY", "STOPPED"].includes(this.task.state.state)) this.task.transition("RUNNING");
if (this.task.state.state === "RUNNING") this.task.transition("PAUSED");
this.task.setProgram("paused", this.xyzacProgram.snapshot().current_line);
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion: this.wasmMotionProjection() }), "verified");
} catch (error) {
return finish(this.error(input, "program_step", String(error?.message ?? error)), "failed");
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
if (this.task.state.state === "READY") {
this.task.setMode("AUTO");
@@ -2280,6 +2336,8 @@ export class SimSession {
// WASM TP, so clear that transport pause before queuing MDI motion;
// the queue itself remains authoritative for execution.
this.wasmMotion.setPaused(false);
const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position);
if (synchronized.status !== "verified") return finish(this.error(input, "mdi_synch", synchronized.code), "failed");
const result = this.xyzacProgram.executeBlock(block);
if (result.status === "verified") {
const controlCode = Number(result.program?.control_code ?? 0);
@@ -2295,6 +2353,7 @@ export class SimSession {
if (result.motion) {
const enqueue = this.wasmMotion.enqueue(result.motion);
if (enqueue.status === "error") return finish(this.error(input, "motion_queue", enqueue.code), "failed");
if (this.wasmMotion.read().state.queue_depth > 0 && this.task.state.state !== "RUNNING") this.task.transition("RUNNING");
}
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
@@ -2334,11 +2393,13 @@ export class SimSession {
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 && this.xyzacProgramKinematicsMode !== XYZAC_MODES.identity) return finish(this.error(input, "home_limit", "home_joint_requires_identity"), "failed");
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");
// AXIS home_all_joints()/home_joint() calls ensure_mode(MANUAL)
// before issuing the homing command.
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, requestedAxis);
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");
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");
@@ -2391,9 +2452,10 @@ export class SimSession {
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
this.wasmMotion.setPaused(false);
const requestedIncrement = input.payload?.increment;
const distance = requestedIncrement === null || input.payload?.continuous === true
? Math.max(Number(this.wasmMotion.jogVelocity(axis)) * this.xyzacMotionCycleTime * 5, Number.EPSILON)
: requestedIncrement ?? 1;
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);
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;