Align AXIS Web control lifecycle with LinuxCNC
This commit is contained in:
@@ -37,14 +37,14 @@ const OPERATION_KINDS = new Set([
|
||||
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
||||
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
|
||||
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP",
|
||||
"HOME", "SET_MODE", "SET_OVERRIDE", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_BRAKE", "TOOL_PREPARE",
|
||||
"HOME", "SET_MODE", "SET_OVERRIDE", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_CONSTANT", "SPINDLE_BRAKE", "TOOL_PREPARE",
|
||||
"TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
|
||||
"SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER",
|
||||
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET",
|
||||
"ENCODER_SET", "COOLANT_COMMAND", "COOLANT_FAULT", "DOOR_SET", "INTERLOCK_SET", "INTERLOCK_RESET", "DEVICE_ESTOP", "DEVICE_ACK_ISSUE", "DEVICE_ACK", "DEVICE_ACK_ABORT",
|
||||
"CONFIGURATION_BATCH",
|
||||
"PYTHON_IMPORT", "PYTHON_CALL", "PYTHON_CAPABILITY",
|
||||
"PROGRAM_STEP", "PROGRAM_SEEK", "MDI_EXECUTE", "PLC_CONFIG", "PLC_SET", "PLC_TRIGGER", "FOLLOWING_ERROR", "IO_COMMAND", "IO_ACK", "IO_TIMEOUT",
|
||||
"PROGRAM_STEP", "PROGRAM_SEEK", "MDI_EXECUTE", "TOUCH_OFF", "TOOL_TOUCH_OFF", "PLC_CONFIG", "PLC_SET", "PLC_TRIGGER", "FOLLOWING_ERROR", "IO_COMMAND", "IO_ACK", "IO_TIMEOUT",
|
||||
"REMAP_REGISTER", "REMAP_CALL", "VCP_REGISTER", "VCP_UPDATE", "FILTER_EXEC",
|
||||
"POWER", "KINEMATICS_SELECT", "VISUAL_CLEAR",
|
||||
]);
|
||||
@@ -53,7 +53,7 @@ const PANEL_GATE_ACTIONS = Object.freeze({
|
||||
PROGRAM_RUN: "run", PROGRAM_STEP: "step", PROGRAM_PAUSE: "pause",
|
||||
PROGRAM_RESUME: "resume", PROGRAM_ABORT: "abort", PROGRAM_RESTART: "restart",
|
||||
JOG_START: "jogStart", JOG_STOP: "jogStop", SET_JOG_SCALE: "jogStart", SET_MAX_VELOCITY: "override", SET_SPINDLE_OVERRIDE: "override", HOME: "home", SET_OVERRIDE: "override",
|
||||
SPINDLE_COMMAND: "spindle", SPINDLE_BRAKE: "spindle", COOLANT_COMMAND: "coolant", TOOL_PREPARE: "tool", TOOL_CHANGE: "tool", PROBE_CONFIG: "probe",
|
||||
SPINDLE_COMMAND: "spindle", SPINDLE_CONSTANT: "spindle", SPINDLE_BRAKE: "spindle", COOLANT_COMMAND: "coolant", TOOL_PREPARE: "tool", TOOL_CHANGE: "tool", PROBE_CONFIG: "probe",
|
||||
ESTOP: "estop", IO_RECOVER: "recover",
|
||||
POWER: "power",
|
||||
});
|
||||
@@ -103,6 +103,8 @@ function canonicalPayload(value) {
|
||||
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalPayload(value[key])}`).join(",")}}`;
|
||||
}
|
||||
|
||||
// Control-flow contract: docs/linuxcnc-axis-web-control-panel-source-logic.md
|
||||
// maps each browser operation to its LinuxCNC AXIS source symbol and gate.
|
||||
export class SimSession {
|
||||
constructor(core, { contextId = CONTEXT_ID, sessionId = "", planner = "queue" } = {}) {
|
||||
this.core = core;
|
||||
@@ -161,6 +163,7 @@ export class SimSession {
|
||||
this.configurationState = null;
|
||||
this.halState = null;
|
||||
this.previewState = null;
|
||||
this.livePathEpoch = 0;
|
||||
this.workspaceFiles = {};
|
||||
call(core, "sim_protocol_reset");
|
||||
if (typeof core.ccall === "function") core.ccall("sim_event_reset", null, [], []);
|
||||
@@ -173,7 +176,8 @@ export class SimSession {
|
||||
// The loaded toolpath is immutable during execution. Avoid cloning all
|
||||
// WASM pose samples into every 10 ms step response; the frontend retains
|
||||
// the canonical path from PROGRAM_LOAD/PROGRAM_RESTART.
|
||||
const lightweightXyzacStep = input.kind === "SIM_STEP" && Boolean(this.xyzacProgram);
|
||||
const xyzacProgramActive = Boolean(this.xyzacProgram);
|
||||
const lightweightXyzacStep = input.kind === "SIM_STEP" && xyzacProgramActive;
|
||||
const traceOnlyStep = lightweightXyzacStep && input.payload?.trace_only === true;
|
||||
const currentMotion = traceOnlyStep && statePayload.wasm_motion ? statePayload.wasm_motion : this.wasmMotion.read();
|
||||
const completedMotion = program.status === 4 && currentMotion.state?.queue_depth === 0;
|
||||
@@ -181,7 +185,21 @@ export class SimSession {
|
||||
// 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 panelSnapshot = motionDelta || traceOnlyStep ? null : this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep });
|
||||
// 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
|
||||
// work. Explicit preview transactions still publish the full replacement.
|
||||
const publishXyzacToolpath = ["PROGRAM_LOAD", "PROGRAM_RESTART", "RECOVER", "SNAPSHOT_REQUEST", "VISUAL_CLEAR"].includes(input.kind);
|
||||
const panelSnapshot = motionDelta || traceOnlyStep ? null : this.panelSnapshot({
|
||||
task,
|
||||
program,
|
||||
input,
|
||||
tick,
|
||||
motionTrace: statePayload.motion_trace,
|
||||
includeToolpath: xyzacProgramActive ? publishXyzacToolpath : !lightweightXyzacStep,
|
||||
includeConfiguration: !lightweightXyzacStep,
|
||||
includeStableRuntime: !lightweightXyzacStep,
|
||||
});
|
||||
const compactProgram = motionDelta ? {
|
||||
schema: program.schema,
|
||||
source: program.source,
|
||||
@@ -337,8 +355,11 @@ export class SimSession {
|
||||
if (!this.xyzacProgram?.source) return { status: "verified" };
|
||||
const source = this.xyzacProgram.source;
|
||||
const path = this.xyzacProgram.path;
|
||||
const synchronizedPosition = this.wasmMotion.read().state.position;
|
||||
const loaded = this.xyzacProgram.load(source, path);
|
||||
if (loaded.status !== "verified") return loaded;
|
||||
const synchronized = this.xyzacProgram.synchronizePosition(synchronizedPosition);
|
||||
if (synchronized.status !== "verified") return synchronized;
|
||||
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
const subroutines = this.loadXyzacSubroutines(path);
|
||||
if (subroutines.status !== "verified") return subroutines;
|
||||
@@ -415,6 +436,7 @@ export class SimSession {
|
||||
schema: "axis-visual.v1",
|
||||
pose: { ...position },
|
||||
joints: Array.isArray(joints) ? [...joints] : [],
|
||||
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,
|
||||
active_progress: wasmMotion.status === "verified" ? wasmMotion.state.progress : 0,
|
||||
@@ -502,6 +524,9 @@ export class SimSession {
|
||||
const program = this.xyzacProgram.snapshot();
|
||||
const io = this.xyzacIo?.canonical?.() ?? {};
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
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 } };
|
||||
}
|
||||
if (input.kind === "KINEMATICS_SELECT" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) {
|
||||
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 } };
|
||||
}
|
||||
@@ -510,10 +535,11 @@ export class SimSession {
|
||||
const ready = Boolean(io.ready) && !estop;
|
||||
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
|
||||
: input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
|
||||
: input.kind === "SPINDLE_COMMAND" || input.kind === "SPINDLE_BRAKE" ? WASM_GATE_COMMANDS.SPINDLE
|
||||
: input.kind === "SPINDLE_COMMAND" || input.kind === "SPINDLE_CONSTANT" || input.kind === "SPINDLE_BRAKE" ? WASM_GATE_COMMANDS.SPINDLE
|
||||
: input.kind === "COOLANT_COMMAND" ? WASM_GATE_COMMANDS.COOLANT
|
||||
: input.kind === "TOOL_PREPARE" || input.kind === "TOOL_CHANGE" ? WASM_GATE_COMMANDS.TOOL
|
||||
: input.kind === "PROBE_CONFIG" ? WASM_GATE_COMMANDS.PROBE
|
||||
@@ -549,9 +575,6 @@ export class SimSession {
|
||||
});
|
||||
if (result === null) return { action, enabled: false, reason: "linuxcnc_wasm_gate_unavailable", taskState: task.state, interpState: "UNKNOWN" };
|
||||
let reason = result === 0 ? "linuxcnc_wasm_authority" : WASM_GATE_REASONS[result] ?? "linuxcnc_wasm_gate_unknown_command";
|
||||
if (input.kind === "PROGRAM_RUN" && Number(program.status) === 5) {
|
||||
return { action, enabled: false, reason: "xyzac_program_aborted_requires_restart", taskState: task.state, interpState: "IDLE", wasm: { io_ready: ready, io_estop: estop, program_status: program.program_status, queue_depth: wasmMotion.state.queue_depth, gate_result: result } };
|
||||
}
|
||||
if (result === 2 && input.kind === "POWER" && input.payload?.active !== true) reason = "xyzac_power_already_off";
|
||||
if (result === 3 && input.kind === "POWER") reason = "xyzac_power_already_on";
|
||||
if (result === 4 && input.kind === "MDI_EXECUTE") reason = "mdi_while_program_active";
|
||||
@@ -576,6 +599,44 @@ export class SimSession {
|
||||
return { ...selected, kinematics: this.xyzacKinematics.snapshot(), wasm_motion: this.wasmMotion.read() };
|
||||
}
|
||||
|
||||
xyzacTouchOff(input) {
|
||||
if (!this.xyzacProgram) return { status: "error", code: "xyzac_program_unavailable" };
|
||||
const payload = input.payload ?? {};
|
||||
const axis = String(payload.axis ?? "").trim().toUpperCase();
|
||||
if (!["X", "Y", "Z", "A", "C"].includes(axis)) return { status: "error", code: "touch_off_axis_invalid" };
|
||||
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+.*)?$/);
|
||||
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 formatted = Number(converted.toFixed(12));
|
||||
const sourceBlock = `G10 L20 ${systemToken} ${axis}${formatted}`;
|
||||
// The LinuxCNC interpreter accepts the G54..G59 word in this command;
|
||||
// the selected WASM interpreter exposes the same operation through its
|
||||
// canonical P-number form (G54=P1, ..., G59.3=P9).
|
||||
const block = `G10 L20 P${systemNumber} ${axis}${formatted}`;
|
||||
const savedMode = this.task.state.mode;
|
||||
try {
|
||||
if (savedMode !== "MDI") this.task.setMode("MDI");
|
||||
this.wasmMotion.setPaused(false);
|
||||
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 } };
|
||||
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) };
|
||||
}
|
||||
}
|
||||
|
||||
stateForCheckpoint() {
|
||||
const state = {
|
||||
context_id: this.contextId,
|
||||
@@ -600,6 +661,7 @@ export class SimSession {
|
||||
wasm_home_limit: this.wasmHomeLimit.exportState(),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
preview: this.previewState,
|
||||
live_path_epoch: this.livePathEpoch,
|
||||
task: this.task.export(),
|
||||
scheduler: this.scheduler.export(),
|
||||
authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(),
|
||||
@@ -634,6 +696,7 @@ export class SimSession {
|
||||
wasm_motion: this.wasmMotion.exportState(),
|
||||
task_authority: this.task.authority(),
|
||||
wasm_home_limit: this.wasmHomeLimit.exportState(),
|
||||
live_path_epoch: this.livePathEpoch,
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
};
|
||||
if (this.xyzacProgram) value.program = { ...this.xyzacProgram.snapshot(), source_text: this.xyzacProgram.source };
|
||||
@@ -711,6 +774,7 @@ export class SimSession {
|
||||
this.configurationState = state.configuration ?? this.configurationState;
|
||||
this.halState = state.hal ?? this.halState;
|
||||
this.previewState = state.preview ?? this.previewState;
|
||||
this.livePathEpoch = Number.isSafeInteger(state.live_path_epoch) ? state.live_path_epoch : this.livePathEpoch;
|
||||
this.python = new PythonBridge(); if (state.python) this.python.restoreState(state.python);
|
||||
const liveEvents = liveTimeline.events.events;
|
||||
const rebasedEvents = liveEvents.map((event, index) => index === liveEvents.length - 1 ? { ...event, tick: record.simulation_tick } : event);
|
||||
@@ -806,6 +870,7 @@ export class SimSession {
|
||||
if (trace.state?.configuration !== undefined) this.configurationState = trace.state.configuration;
|
||||
if (trace.state?.hal !== undefined) this.halState = trace.state.hal;
|
||||
if (trace.state?.preview !== undefined) this.previewState = trace.state.preview;
|
||||
if (Number.isSafeInteger(trace.state?.live_path_epoch)) this.livePathEpoch = trace.state.live_path_epoch;
|
||||
if (trace.state?.plc) this.plc.restore(trace.state.plc);
|
||||
if (trace.state?.hal_runtime) this.hal.restore(trace.state.hal_runtime);
|
||||
if (trace.state?.device_io) this.deviceIo.restore(trace.state.device_io);
|
||||
@@ -856,6 +921,7 @@ export class SimSession {
|
||||
this.configurationState = checkpoint.state.configuration ?? this.configurationState;
|
||||
this.halState = checkpoint.state.hal ?? this.halState;
|
||||
this.previewState = checkpoint.state.preview ?? this.previewState;
|
||||
this.livePathEpoch = Number.isSafeInteger(checkpoint.state.live_path_epoch) ? checkpoint.state.live_path_epoch : this.livePathEpoch;
|
||||
this.python = new PythonBridge(); if (checkpoint.state.python) this.python.restoreState(checkpoint.state.python);
|
||||
this.timeline = new ReplayTimeline(checkpoint.timeline);
|
||||
if (checkpoint.state.hal_runtime?.schema === "wasm_xyzac_hal.v1" && this.xyzacIo) this.xyzacIo.halCommand({ action: "step", ticks: checkpoint.state.hal_runtime.tick ?? 0 });
|
||||
@@ -1104,6 +1170,8 @@ export class SimSession {
|
||||
this.configurationState = result.state?.configuration ?? checkpoint?.state?.configuration ?? null;
|
||||
this.halState = result.state?.hal ?? checkpoint?.state?.hal ?? null;
|
||||
this.previewState = result.state?.preview ?? checkpoint?.state?.preview ?? null;
|
||||
const recoveredLivePathEpoch = result.state?.live_path_epoch ?? checkpoint?.state?.live_path_epoch;
|
||||
if (Number.isSafeInteger(recoveredLivePathEpoch)) this.livePathEpoch = recoveredLivePathEpoch;
|
||||
this.python = new PythonBridge();
|
||||
if (result.state?.python) this.python.restoreState(result.state.python);
|
||||
this.program = new ProgramRunner(this.core);
|
||||
@@ -1322,9 +1390,9 @@ 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.previewState = null;
|
||||
this.livePathEpoch += 1;
|
||||
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, visual_only: true, before, after, operation_id: input.operation_id ?? null }), "verified");
|
||||
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");
|
||||
};
|
||||
|
||||
switch (input.kind) {
|
||||
@@ -1929,6 +1997,17 @@ export class SimSession {
|
||||
case "PROGRAM_LOAD": {
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed");
|
||||
const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source;
|
||||
this.livePathEpoch += 1;
|
||||
const savedMode = this.task.state.mode;
|
||||
const restoreMode = () => {
|
||||
if (savedMode !== "MANUAL" && this.task.state.mode !== savedMode) this.task.setMode(savedMode);
|
||||
};
|
||||
// AXIS open_file_guts() enters MANUAL before the replacement setup,
|
||||
// then restores the tab-selected task mode before PLAN_OPEN. The
|
||||
// final restore below mirrors set_mode_from_tab after the UI refresh.
|
||||
try { if (savedMode !== "MANUAL") this.task.setMode("MANUAL"); } catch (error) {
|
||||
return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
// PROGRAM_LOAD is a replacement transaction. Clear the previous
|
||||
// canonical preview before asking the WASM interpreter to parse the
|
||||
// new source, so a parse/subroutine error cannot re-publish the old
|
||||
@@ -1952,9 +2031,12 @@ export class SimSession {
|
||||
this.xyzacDeferredPhysicalPose = null;
|
||||
this.xyzacDeferredPhysicalPoseApplied = false;
|
||||
this.xyzacAppendDeferredOnActive = false;
|
||||
restoreMode();
|
||||
const synchronizedPosition = this.wasmMotion.read().state.position;
|
||||
const result = this.xyzacProgram.load(source, input.payload?.path ?? null);
|
||||
if (result.status === "verified") { const synchronized = this.xyzacProgram.synchronizePosition(synchronizedPosition); if (synchronized.status !== "verified") { restoreMode(); return finish(this.error(input, "program_synch", synchronized.code), "failed"); } }
|
||||
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(input.payload?.path); if (subroutines.status !== "verified") return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); }
|
||||
if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(input.payload?.path); if (subroutines.status !== "verified") { restoreMode(); return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); } }
|
||||
const preview = result.status === "verified" ? this.xyzacProgram.preview() : result;
|
||||
if (result.status === "verified" && preview.status === "verified") {
|
||||
this.previewState = preview;
|
||||
@@ -1964,9 +2046,11 @@ export class SimSession {
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
this.task.setProgram("loaded", 0);
|
||||
}
|
||||
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);
|
||||
}
|
||||
restoreMode();
|
||||
const result = this.program.load(source, input.payload?.path ?? null);
|
||||
if (result.status === "verified") {
|
||||
this.authoritative.reset();
|
||||
@@ -1978,6 +2062,7 @@ export class SimSession {
|
||||
result.preview = preview.preview ?? { status: "error", code: preview.code ?? "program_preview_failed" };
|
||||
this.previewState = result.preview;
|
||||
}
|
||||
restoreMode();
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
|
||||
...result,
|
||||
task: this.task.export(),
|
||||
@@ -1990,12 +2075,14 @@ export class SimSession {
|
||||
}
|
||||
case "PROGRAM_RUN": {
|
||||
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); }
|
||||
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");
|
||||
if (result.status === "verified") { this.livePathEpoch += 1; 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); }
|
||||
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();
|
||||
if (result.status === "verified") {
|
||||
this.livePathEpoch += 1;
|
||||
this.task.setMode("AUTO");
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(false);
|
||||
@@ -2094,14 +2181,13 @@ export class SimSession {
|
||||
this.xyzacDeferredPhysicalPose = null;
|
||||
this.xyzacDeferredPhysicalPoseApplied = false;
|
||||
this.xyzacAppendDeferredOnActive = false;
|
||||
const source = this.xyzacProgram.source;
|
||||
const result = this.xyzacProgram.load(source, this.xyzacProgram.path);
|
||||
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(this.xyzacProgram.path); if (subroutines.status !== "verified") return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); const preview = this.xyzacProgram.preview(); if (preview.status === "verified") this.previewState = preview; this.wasmMotion.reset(); this.wasmMotion.setPaused(false); this.task.recover(); this.task.setMode("AUTO"); this.task.setProgram("loaded", 0); }
|
||||
const result = this.resetXyzacProgramToLoaded();
|
||||
if (result.status === "verified") { this.livePathEpoch += 1; this.wasmMotion.abort(); this.wasmMotion.setPaused(false); this.task.recover(); this.task.setMode("AUTO"); this.task.setProgram("loaded", 0); }
|
||||
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");
|
||||
}
|
||||
const result = this.program.restart();
|
||||
if (result.status === "verified") {
|
||||
this.livePathEpoch += 1;
|
||||
if (["ESTOP", "FAULT"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_restart_faulted"), "failed");
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
@@ -2225,6 +2311,17 @@ export class SimSession {
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "TOUCH_OFF": case "TOOL_TOUCH_OFF": {
|
||||
if (input.kind === "TOOL_TOUCH_OFF") return finish(this.error(input, "abi_capability", "blocked_by_abi_tool_offsets"), "failed");
|
||||
const result = this.xyzacTouchOff(input);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
|
||||
...result,
|
||||
task: this.task.export(),
|
||||
motion_authority: this.xyzacProgram ? "wasm" : this.motionAuthority,
|
||||
command: input.kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "KINEMATICS_SELECT": {
|
||||
if (!this.xyzacKinematics) return finish(this.error(input, "kinematics", "xyzac_kinematics_unavailable"), "failed");
|
||||
const mode = Number(input.payload?.type);
|
||||
@@ -2239,7 +2336,12 @@ 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 home = this.wasmHomeLimit.home(input.seq, input.payload?.all ? null : input.payload?.axis ?? null);
|
||||
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");
|
||||
// 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);
|
||||
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");
|
||||
@@ -2291,7 +2393,10 @@ export class SimSession {
|
||||
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");
|
||||
this.wasmMotion.setPaused(false);
|
||||
const distance = input.payload?.increment ?? 1;
|
||||
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 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;
|
||||
@@ -2308,7 +2413,7 @@ export class SimSession {
|
||||
if (result.status === "error" && result.state?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "SET_SPINDLE_OVERRIDE": case "SPINDLE_COMMAND": case "SPINDLE_BRAKE": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SET_SPINDLE_OVERRIDE": case "SPINDLE_COMMAND": case "SPINDLE_CONSTANT": case "SPINDLE_BRAKE": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":
|
||||
case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE":
|
||||
case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT":
|
||||
|
||||
Reference in New Issue
Block a user