Make machine configuration authoritative and restore native checkpoints exactly

This commit is contained in:
2026-09-18 09:09:41 +08:00
parent 6a69511276
commit 4a568f1f4c
94 changed files with 10882 additions and 301 deletions

View File

@@ -61,7 +61,7 @@ export function normalizeAxisState(input = {}) {
const hasEditor = input.has_editor !== false;
return Object.freeze({
taskState, interpState, mode, motionMode, programStatus, queue, queueLimit,
taskFile, hasEditor, onAnyLimit: Boolean(limits), toolLoaded: toolNumber > 0,
taskFile, hasEditor, onAnyLimit: Boolean(limits), toolLoaded: toolNumber > 0, motionQueueDepth: number(motion.queue_depth),
allHomed: homed.length > 0 && homed.every(Boolean), homed, fault,
});
}
@@ -101,6 +101,13 @@ export function axisGate(action, input = {}) {
case "restart": enabled = machineOn && idle && state.taskFile; reason = enabled ? "axis_restart_available" : !machineOn ? "axis_machine_off" : !state.taskFile ? "axis_taskfile_missing" : "axis_restart_state_gate"; break;
case "mdi": enabled = machineOn && state.queue < state.queueLimit && (idle || state.mode === "MDI"); reason = enabled ? "axis_mdi_available" : estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : state.queue >= state.queueLimit ? "axis_mdi_queue_full" : "axis_interp_running"; break;
case "jogStart": case "jogStop": case "home": case "spindle": case "coolant": enabled = manualOk && state.mode === "MANUAL"; reason = enabled ? "axis_manual_available" : estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : "axis_manual_state_gate"; break;
// Direct operator tool commands must wait for the interpreter and motion.
// emcTaskCheckPreconditions waits for IO on PREPARE and motion+IO on LOAD;
// the IO handler below this gate owns prepared/changed acknowledgements.
case "tool": case "toolPrepare": case "toolChange":
enabled = machineOn && idle && state.motionQueueDepth === 0 && ["MANUAL", "MDI"].includes(state.mode);
reason = estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : !idle ? "axis_interp_running" : state.motionQueueDepth > 0 ? "axis_motion_active" : !["MANUAL", "MDI"].includes(state.mode) ? "axis_tool_operator_mode_required" : "axis_tool_available";
break;
case "touchOff": enabled = manualOk && state.mode === "MANUAL" && trajectoryTouchOffAllowed; reason = !manualOk ? "axis_manual_state_gate" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : !trajectoryTouchOffAllowed ? "axis_joint_mode_required" : "axis_touch_off_available"; break;
case "toolTouchOff": enabled = manualOk && state.mode === "MANUAL" && trajectoryTouchOffAllowed && state.toolLoaded; reason = !state.toolLoaded ? "axis_tool_missing" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : !trajectoryTouchOffAllowed ? "axis_joint_mode_required" : !manualOk ? "axis_manual_state_gate" : "axis_tool_touch_off_available"; break;
case "overrideLimits": enabled = state.onAnyLimit; reason = enabled ? "axis_limit_override_available" : "axis_no_active_limit"; break;

View File

@@ -1,7 +1,8 @@
export const AXIS_ASSET_ROOT = "/linuxcnc/share/axis/images";
import { WEB_SETTINGS, axisImagePath } from "../workspace/web-settings.mjs";
export const AXIS_ASSET_ROOT = `./${WEB_SETTINGS.assets.axis_images}`;
export function axisAsset(name) {
return `${AXIS_ASSET_ROOT}/${name}`;
return axisImagePath(name);
}
export function appendAxisIcon(document, button, asset, label) {

View File

@@ -31,4 +31,14 @@ assert.equal(axisGate("overrideLimits", limit).enabled, true);
assert.equal(axisGateMatrix(ready).run.reason, "axis_run_available");
assert.equal(AXIS_COMMANDS.run, "PROGRAM_RUN");
assert.equal(AXIS_BOUNDARIES.physicalHardware, "REAL-EXCLUDED");
for (const action of ["tool", "toolPrepare", "toolChange"]) {
assert.equal(axisGate(action, ready).enabled, true);
assert.equal(axisGate(action, running).enabled, false);
assert.equal(axisGate(action, paused).enabled, false);
assert.equal(axisGate(action, estop).enabled, false);
assert.equal(axisGate(action, { ...ready, task: { ...ready.task, state: "OFF" } }).enabled, false);
assert.equal(axisGate(action, { ...ready, motion: { ...ready.motion, queue_depth: 1 } }).reason, "axis_motion_active");
assert.equal(axisGate(action, { ...ready, devices: { fault: true } }).reason, "axis_fault_active");
assert.equal(axisGate(action, { ...ready, task: { ...ready.task, mode: "AUTO" } }).enabled, false);
}
console.log(JSON.stringify({ schema: "axis-contract-sample.v1", status: "verified", commands: Object.keys(AXIS_COMMANDS).length, matrix: Object.keys(axisGateMatrix(ready)).length, blocked_reasons: [axisGate("mdi", queueFull).reason, axisGate("jogStart", estop).reason] }));