Add INI-driven XYZAC and XYZBC parametric machine design

This commit is contained in:
2026-09-17 19:16:48 +08:00
parent 9232f29027
commit e4600b34e3
31 changed files with 2359 additions and 29 deletions

View File

@@ -32,6 +32,7 @@ import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
import { WasmProgramRuntime } from "./wasm-program-runtime.mjs";
import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
import { machineDefinition } from "../workspace/machine-definitions.mjs";
import { readTrtGeometry, TRT_GEOMETRY_KEYS } from "../workspace/trt-geometry.mjs";
const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
@@ -196,7 +197,7 @@ export class SimSession {
// 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 publishXyzacToolpath = ["LOAD_WORKSPACE", "PROGRAM_LOAD", "PROGRAM_RESTART", "RECOVER", "SNAPSHOT_REQUEST", "VISUAL_CLEAR"].includes(input.kind);
const panelSnapshot = motionDelta || traceOnlyStep ? null : this.panelSnapshot({
task,
program,
@@ -600,6 +601,9 @@ export class SimSession {
const program = this.xyzacProgram.snapshot();
const io = this.xyzacIo?.canonical?.() ?? {};
const wasmMotion = this.wasmMotion.read();
if (input.kind === "LOAD_WORKSPACE" && (io.ready || ["running", "paused"].includes(program.program_status) || Number(wasmMotion.state?.queue_depth) > 0)) {
return { enabled: false, reason: "trt_configuration_requires_power_off_idle" };
}
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) {
@@ -830,6 +834,12 @@ export class SimSession {
return { ...compact, checkpoint_hash: fnv1a64(compact) };
}
validateRecoveryGeometry(state) {
if (!this.xyzacKinematics) return;
const current = this.xyzacKinematics.snapshot();
if (!state?.kinematics || state.kinematics.coordinates !== current.coordinates || TRT_GEOMETRY_KEYS.some((key) => Number(state.kinematics.offsets?.[key] ?? 0) !== current.offsets[key])) throw new Error("trt_recovery_geometry_mismatch");
}
restoreCompactPowerOff(record, transportSeq) {
if (!record || record.schema !== "xyzac-power-off-checkpoint.v1" || record.context_id !== this.contextId || !record.state) return { status: "blocked", code: "runtime_checkpoint_schema", reason: "power_off_checkpoint_schema_invalid" };
if (fnv1a64(record.state) !== record.state_hash || fnv1a64(Object.fromEntries(Object.entries(record).filter(([key]) => key !== "checkpoint_hash"))) !== record.checkpoint_hash) return { status: "blocked", code: "runtime_checkpoint_hash", reason: "power_off_checkpoint_hash_mismatch" };
@@ -839,6 +849,7 @@ export class SimSession {
const liveLedger = this.ledger;
const liveOperations = [...this.operations.values()];
try {
this.validateRecoveryGeometry(state);
if (!this.xyzacProgram || !state.program?.source_text) throw new Error("power_off_program_missing");
const loaded = this.xyzacProgram.load(state.program.source_text, state.program.path ?? null);
if (loaded.status !== "verified") throw new Error(loaded.code);
@@ -941,6 +952,7 @@ export class SimSession {
importTrace(trace) {
const validated = validateTrace(trace, { contextId: this.contextId });
this.validateRecoveryGeometry(trace.state);
this.timeline = validated.timeline;
this.created = Boolean(trace.state?.created);
if (trace.state?.ledger?.entries) this.ledger = new OperationLedger(trace.state.ledger.entries);
@@ -977,6 +989,7 @@ export class SimSession {
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
try {
validateCheckpoint(checkpoint, { contextId: this.contextId });
this.validateRecoveryGeometry(checkpoint.state);
} catch (error) {
return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ reason: String(error?.message ?? error) }] };
}
@@ -1594,7 +1607,15 @@ export class SimSession {
const rootPath = input.payload?.root_path;
if (!text(rootPath) || !rootPath.startsWith("/") || rootPath.includes("..")) return finish(this.error(input, "workspace_path", "invalid_workspace_path"), "failed");
let startupLoaded = false;
const configuration = input.payload?.configuration ?? null;
let configuration = input.payload?.configuration ?? null;
if (machineDefinition(configuration?.entrypoint) || machineDefinition(configuration?.kinematics?.type)) {
try {
const geometry = readTrtGeometry(configuration);
configuration = { ...configuration, kinematics: { ...configuration.kinematics, offsets: geometry.offsets } };
} catch (error) {
return finish(this.error(input, "trt_geometry", error.message), "failed");
}
}
if (configuration) {
this.machineDefinition = machineDefinition(configuration.entrypoint) ?? machineDefinition(configuration.kinematics?.type) ?? this.machineDefinition;
this.workspaceFiles = { ...(configuration.files ?? configuration.workspace ?? configuration.workspace_files ?? {}) };
@@ -1669,6 +1690,9 @@ 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.previewState = null;
this.pendingXyzacMotion = null;
this.clearLivePlot();
this.homingHistory = [];
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);

View File

@@ -37,5 +37,6 @@ for (const coordinates of ["XYZAC", "XYZBC"]) {
assert.ok(Number.isFinite(result.payload.panel_snapshot.visual.pose[rotary]));
assert.equal(result.payload.panel_snapshot.visual.machine_model.coordinate_system, coordinates);
rows.push({ coordinates, program: programPath, status: result.payload.program.program_status, rotary });
assert.equal(send("POWER", { active: false }).kind, "COMMAND_RESULT");
}
console.log(JSON.stringify({ schema: "boat-trt-programs.v1", status: "verified", rows }));

View File

@@ -35,7 +35,9 @@ assert.equal(loaded.payload.motion_authority, "wasm");
assert.equal(loaded.payload.configuration.state, "TASK_READY");
assert.equal(loaded.payload.panel_snapshot.kinematics.joint_count, 5);
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
assert.equal(send("HOME", "home-all", { all: true }).kind, "COMMAND_RESULT");
for (let tick = 0; tick < 500 && !session.wasmHomeLimit.read().state.homed; tick++) send("SIM_STEP", `home-step-${tick}`, { cycles: 1 });
assert.equal(session.wasmHomeLimit.read().state.homed, true);
const switchRows = [];
for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2]]) {