Keep long G-code execution running past full planner queue

This commit is contained in:
2026-08-30 06:34:59 +08:00
parent d87366eb75
commit 2d6d8bd1c2
5 changed files with 32 additions and 15 deletions

View File

@@ -2,18 +2,18 @@ const EVENT_SCHEMA = "event-log.v1";
const SNAPSHOT_SCHEMA = "snapshot.v1";
const REPLAY_SCHEMA = "replay-timeline.v1";
export function canonicalJson(value) {
export function canonicalJson(value, path = "$") {
if (value === null) return "null";
if (typeof value === "number") {
if (!Number.isFinite(value)) throw new Error("canonical_nonfinite");
return JSON.stringify(value);
}
if (typeof value !== "object") {
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") throw new Error("canonical_unsupported");
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") throw new Error(`canonical_unsupported:${path}`);
return JSON.stringify(value);
}
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
if (Array.isArray(value)) return `[${value.map((item, index) => canonicalJson(item, `${path}[${index}]`)).join(",")}]`;
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key], `${path}.${key}`)}`).join(",")}}`;
}
export function fnv1a64(value) {

View File

@@ -151,7 +151,10 @@ export class SimSession {
const lightweightXyzacStep = input.kind === "SIM_STEP" && Boolean(this.xyzacProgram);
const currentMotion = this.wasmMotion.read();
const completedMotion = program.status === 4 && currentMotion.state?.queue_depth === 0;
const motionDelta = lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion;
// 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 panelSnapshot = motionDelta ? null : this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep });
const compactProgram = motionDelta ? {
schema: program.schema,
@@ -1415,6 +1418,11 @@ export class SimSession {
if (this.xyzacProgram) {
const wasmState = this.xyzacProgram.snapshot();
if (![2, 3].includes(wasmState.status)) return;
// LinuxCNC's task/interpreter blocks on a full trajectory
// planner queue. Let the motion phase consume one segment
// before reading another source line instead of converting
// normal planner backpressure into a program fault.
if (this.wasmMotion.read().state.queue_depth >= this.wasmMotion.queueCapacity) return;
if (this.pendingProgramControl !== 0) {
if (this.wasmMotion.read().state.queue_depth !== 0) return;
const current = this.wasmMotion.read().state.position;

View File

@@ -44,13 +44,18 @@ assert.equal(send("POWER", { active: true }).kind, "COMMAND_RESULT");
assert.equal(send("HOME", { all: true }).kind, "COMMAND_RESULT");
const run = send("PROGRAM_RUN");
assert.equal(run.kind, "COMMAND_RESULT");
const stepped = send("SIM_STEP", { cycles: 16, response: "motion_delta" });
let stepped = null;
for (let cycle = 0; cycle < 300; cycle += 1) {
stepped = send("SIM_STEP", { cycles: 1, response: "motion_delta" });
assert.equal(stepped.kind, "COMMAND_RESULT");
}
assert.equal(stepped.kind, "COMMAND_RESULT");
assert.equal(stepped.payload.program.program_status, "running");
assert.ok(stepped.payload.program.current_line > 0);
assert.ok(stepped.payload.program.current_line > 82);
assert.ok(stepped.payload.execution_trace.length > 0);
assert.ok(stepped.payload.program.execution_line_text.length > 0);
assert.equal(stepped.payload.motion_authority, "wasm");
assert.equal(Object.hasOwn(stepped.payload.xyzac_ledger.entries.at(-1), "response"), false);
console.log(JSON.stringify({
schema: "xyzac-3d-chips-program.v1",

View File

@@ -45,10 +45,11 @@ export class XyzacCommandLedger extends OperationLedger {
return { status: "reconcile_required", operation_id: operationId, retry_allowed: false, reconciliation };
}
audit() {
audit({ includeResponses = true } = {}) {
const exported = super.export();
const pending = this.pending();
const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries: exported.entries, pending_count: pending.length };
const entries = includeResponses ? exported.entries : exported.entries.map((entry) => { const { response, ...summary } = entry; return summary; });
const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries, pending_count: pending.length };
return { ...unsigned, state_hash: fnv1a64(unsigned) };
}
}

View File

@@ -28,12 +28,15 @@ export class XyzacSession extends SimSession {
accept(input) {
const response = super.accept(input);
if (response?.payload && typeof response.payload === "object") {
response.payload.xyzac_session = this.authority();
response.payload.xyzac_ledger = this.ledger.audit();
response.payload.xyzac_scheduler = this.scheduler.export();
}
return response;
if (!response?.payload || typeof response.payload !== "object") return response;
// super.accept stores the response in the operation ledger. Enrich a
// shallow client copy so the next ledger audit cannot recursively embed
// every previous response inside the stored response object.
const payload = { ...response.payload };
payload.xyzac_session = this.authority();
payload.xyzac_ledger = this.ledger.audit({ includeResponses: input.kind !== "SIM_STEP" });
payload.xyzac_scheduler = this.scheduler.export();
return { ...response, payload };
}
}