Optimize XYZAC motion updates and persist power-off recovery
This commit is contained in:
@@ -149,7 +149,44 @@ export class SimSession {
|
||||
// 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 panelSnapshot = this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep });
|
||||
const currentMotion = this.wasmMotion.read();
|
||||
const completedMotion = program.status === 4 && currentMotion.state?.queue_depth === 0;
|
||||
const motionDelta = 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,
|
||||
source: program.source,
|
||||
status: program.status,
|
||||
program_status: program.program_status,
|
||||
loaded: program.loaded,
|
||||
current_line: program.current_line,
|
||||
line: program.line,
|
||||
path: program.path,
|
||||
source_hash: program.source_hash,
|
||||
state_hash: program.state_hash,
|
||||
} : null;
|
||||
const trace = Array.isArray(statePayload.motion_trace) ? statePayload.motion_trace : [];
|
||||
const motionSample = motionDelta ? (trace.at(-1) ?? {
|
||||
tick,
|
||||
pose: currentMotion.state?.position ?? null,
|
||||
joints: [],
|
||||
active_segment_id: currentMotion.state?.active_id ?? 0,
|
||||
active_progress: currentMotion.state?.progress ?? 0,
|
||||
}) : null;
|
||||
const responsePayload = motionDelta ? {
|
||||
status: statePayload.status,
|
||||
cycles: statePayload.cycles,
|
||||
motion_trace: trace,
|
||||
motion_sample: motionSample,
|
||||
snapshot_mode: "motion_delta",
|
||||
motion_authority: statePayload.motion_authority ?? this.motionAuthority,
|
||||
task,
|
||||
task_authority: statePayload.task_authority ?? this.task.authority(),
|
||||
wasm_motion: statePayload.wasm_motion ?? currentMotion,
|
||||
motion: statePayload.motion ?? this.motionProjection(),
|
||||
program: compactProgram,
|
||||
state_hash: statePayload.state_hash ?? null,
|
||||
} : statePayload;
|
||||
return {
|
||||
schema_version: PROTOCOL_SCHEMA,
|
||||
kind,
|
||||
@@ -159,12 +196,12 @@ export class SimSession {
|
||||
...(text(input.operation_id) ? { operation_id: input.operation_id } : {}),
|
||||
simulation_tick: tick,
|
||||
payload: {
|
||||
...statePayload,
|
||||
panel_snapshot: panelSnapshot,
|
||||
motion_authority: statePayload.motion_authority ?? this.motionAuthority,
|
||||
task_authority: statePayload.task_authority ?? this.task.authority(),
|
||||
...responsePayload,
|
||||
...(panelSnapshot ? { panel_snapshot: panelSnapshot } : {}),
|
||||
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
|
||||
task_authority: responsePayload.task_authority ?? this.task.authority(),
|
||||
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}),
|
||||
axis_state: axisState({ task, program, motion: this.motionProjection(), devices: this.wasmDeviceProjection(), io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }),
|
||||
axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -516,6 +553,123 @@ export class SimSession {
|
||||
});
|
||||
}
|
||||
|
||||
createPowerOffCheckpoint() {
|
||||
const checkpoint = this.createCheckpoint();
|
||||
const state = structuredClone(checkpoint.state);
|
||||
// Operation history and replay snapshots are useful for an explicit
|
||||
// recovery command, but are redundant for a power-off restart. Omitting
|
||||
// them keeps the browser durable record bounded during long JOG runs.
|
||||
delete state.ledger;
|
||||
const compact = {
|
||||
schema: "xyzac-power-off-checkpoint.v1",
|
||||
context_id: this.contextId,
|
||||
simulation_tick: checkpoint.simulation_tick,
|
||||
state,
|
||||
state_hash: fnv1a64(state),
|
||||
};
|
||||
return { ...compact, checkpoint_hash: fnv1a64(compact) };
|
||||
}
|
||||
|
||||
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" };
|
||||
if (!Number.isSafeInteger(transportSeq) || transportSeq < 1 || !Number.isSafeInteger(record.simulation_tick) || record.simulation_tick < 0) return { status: "blocked", code: "runtime_checkpoint_range", reason: "power_off_checkpoint_range_invalid" };
|
||||
const state = record.state;
|
||||
const liveTimeline = this.timeline.export();
|
||||
const liveLedger = this.ledger;
|
||||
const liveOperations = [...this.operations.values()];
|
||||
try {
|
||||
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);
|
||||
const subroutines = this.loadXyzacSubroutines(state.program.path ?? null);
|
||||
if (subroutines.status !== "verified") throw new Error(subroutines.code);
|
||||
if (this.xyzacProgram.restoreState(state.program).status === "error") throw new Error("power_off_program_restore_failed");
|
||||
if (state.wasm_motion) this.wasmMotion.restoreState(state.wasm_motion);
|
||||
if (state.wasm_home_limit) this.wasmHomeLimit.restoreState(state.wasm_home_limit);
|
||||
if (state.device_io && this.xyzacDeviceIo) this.xyzacDeviceIo.restoreState(state.device_io);
|
||||
if (state.devices && this.xyzacIo) this.xyzacIo.restoreState(state.devices);
|
||||
if (state.kinematics && this.xyzacKinematics) this.xyzacKinematics.restoreState(state.kinematics);
|
||||
if (state.task) {
|
||||
const task = { ...state.task, session_id: this.sessionId, seq: transportSeq, simulation_tick: record.simulation_tick };
|
||||
delete task.state_hash;
|
||||
task.state_hash = fnv1a64(task);
|
||||
this.task.restore(task);
|
||||
}
|
||||
if (state.scheduler) this.scheduler.restore(state.scheduler);
|
||||
this.plc = new PlcRuntime(); if (state.plc) this.plc.restore(state.plc);
|
||||
this.hal = new HalRuntime(); if (state.hal_runtime?.schema === "sim_hal_runtime.v1") this.hal.restore(state.hal_runtime);
|
||||
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId }); if (state.configuration_batch) this.configurationBatch.restore(state.configuration_batch);
|
||||
this.ioAck = new IoAckTracker(); if (state.io_ack) this.ioAck.restore(state.io_ack);
|
||||
this.callbacks = new PythonCallbacks(); if (state.callbacks) this.callbacks.restore(state.callbacks);
|
||||
this.configurationState = state.configuration ?? this.configurationState;
|
||||
this.halState = state.hal ?? this.halState;
|
||||
this.previewState = state.preview ?? this.previewState;
|
||||
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);
|
||||
this.timeline = new ReplayTimeline({ schema: liveTimeline.schema, events: { ...liveTimeline.events, tick: record.simulation_tick, events: rebasedEvents }, snapshots: { schema: "snapshot.v1", snapshots: [] } });
|
||||
this.tick = record.simulation_tick;
|
||||
const protocolTick = call(this.core, "sim_protocol_set_tick", record.simulation_tick);
|
||||
if (protocolTick !== 0) throw new Error(`protocol_tick_${protocolTick}`);
|
||||
this.timeline.checkpoint(transportSeq, record.simulation_tick, this.stateForCheckpoint(), null);
|
||||
this.operations = new Map(liveOperations.map((operation) => [operation.operation_id, operation]));
|
||||
this.ledger = liveLedger;
|
||||
return { status: "verified", storage: "browser_client_storage", checkpoint_hash: record.checkpoint_hash, simulation_tick: record.simulation_tick, rebased_seq: transportSeq };
|
||||
} catch (error) {
|
||||
this.ledger = liveLedger;
|
||||
this.operations = new Map(liveOperations.map((operation) => [operation.operation_id, operation]));
|
||||
return { status: "blocked", code: "runtime_checkpoint_restore_failed", reason: String(error?.message ?? error) };
|
||||
}
|
||||
}
|
||||
|
||||
restorePersistedPowerOff(checkpoint, transportSeq) {
|
||||
if (!checkpoint || !Number.isSafeInteger(transportSeq) || transportSeq < 1) return { status: "blocked", code: "runtime_checkpoint_input_invalid", reason: "checkpoint_or_sequence_invalid" };
|
||||
if (checkpoint.schema === "xyzac-power-off-checkpoint.v1") return this.restoreCompactPowerOff(checkpoint, transportSeq);
|
||||
const liveTimeline = this.timeline.export();
|
||||
const liveLedger = this.ledger;
|
||||
const liveOperations = [...this.operations.values()];
|
||||
// A browser restart creates a new transport session id. Adapt only the
|
||||
// task identity and recompute checkpoint digests before restoring so the
|
||||
// WASM task canonical (which was initialized with the new id) remains
|
||||
// self-consistent.
|
||||
let checkpointForRestore = checkpoint;
|
||||
if (checkpoint.state?.task?.session_id && checkpoint.state.task.session_id !== this.sessionId) {
|
||||
checkpointForRestore = structuredClone(checkpoint);
|
||||
checkpointForRestore.state.task.session_id = this.sessionId;
|
||||
checkpointForRestore.state.task.state_hash = fnv1a64(Object.fromEntries(Object.entries(checkpointForRestore.state.task).filter(([key]) => key !== "state_hash")));
|
||||
checkpointForRestore.state_hash = fnv1a64(checkpointForRestore.state);
|
||||
const unsigned = Object.fromEntries(Object.entries(checkpointForRestore).filter(([key]) => key !== "checkpoint_hash"));
|
||||
checkpointForRestore.checkpoint_hash = fnv1a64(unsigned);
|
||||
}
|
||||
const restored = this.restoreCheckpoint(checkpointForRestore);
|
||||
if (restored.status !== "recovered") return restored;
|
||||
const restoredTick = this.tick;
|
||||
const restoredTask = this.task.export();
|
||||
const rebasedTask = { ...restoredTask, session_id: this.sessionId, seq: transportSeq, simulation_tick: restoredTick };
|
||||
delete rebasedTask.state_hash;
|
||||
rebasedTask.state_hash = fnv1a64(rebasedTask);
|
||||
try {
|
||||
this.task.restore(rebasedTask);
|
||||
const liveEvents = liveTimeline.events.events;
|
||||
const rebasedEvents = liveEvents.map((event, index) => index === liveEvents.length - 1 ? { ...event, tick: restoredTick } : event);
|
||||
this.timeline = new ReplayTimeline({
|
||||
schema: liveTimeline.schema,
|
||||
events: { ...liveTimeline.events, tick: restoredTick, events: rebasedEvents },
|
||||
snapshots: { schema: "snapshot.v1", snapshots: [] },
|
||||
});
|
||||
this.tick = restoredTick;
|
||||
const protocolTick = call(this.core, "sim_protocol_set_tick", restoredTick);
|
||||
if (protocolTick !== 0) throw new Error(`protocol_tick_${protocolTick}`);
|
||||
this.timeline.checkpoint(transportSeq, restoredTick, this.stateForCheckpoint(), null);
|
||||
this.operations = new Map(liveOperations.map((operation) => [operation.operation_id, operation]));
|
||||
this.ledger = liveLedger;
|
||||
return { status: "verified", storage: "browser_client_storage", checkpoint_hash: checkpoint.checkpoint_hash, simulation_tick: restoredTick, rebased_seq: transportSeq };
|
||||
} catch (error) {
|
||||
return { status: "blocked", code: "runtime_checkpoint_rebase_failed", reason: String(error?.message ?? error) };
|
||||
}
|
||||
}
|
||||
|
||||
exportTrace() {
|
||||
const latest = this.timeline.snapshots.latest();
|
||||
if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) {
|
||||
@@ -946,6 +1100,9 @@ export class SimSession {
|
||||
code: "axis_gate",
|
||||
message_key: gate.reason,
|
||||
gate_reason: gate.reason,
|
||||
reason: gate.reason,
|
||||
error_code: "axis_gate",
|
||||
source: "linuxcnc-wasm-worker",
|
||||
retryable: false,
|
||||
operation_id: input.operation_id ?? null,
|
||||
});
|
||||
@@ -1067,6 +1224,8 @@ export class SimSession {
|
||||
case "POWER": {
|
||||
if (this.xyzacProgram) {
|
||||
const result = this.xyzacIo.command("POWER", input.payload ?? {});
|
||||
let recoveryCheckpoint = null;
|
||||
let recoveryPersistence = { status: "skipped", reason: "power_on" };
|
||||
if (result.status === "verified") {
|
||||
if (input.payload?.active === false) {
|
||||
/* LinuxCNC emcTaskSetState(OFF) aborts motion, clears the
|
||||
@@ -1084,9 +1243,17 @@ export class SimSession {
|
||||
if (["READY", "RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
|
||||
}
|
||||
} catch { /* the WASM IO state remains authoritative */ }
|
||||
if (input.payload?.active === false) {
|
||||
try {
|
||||
recoveryCheckpoint = this.createPowerOffCheckpoint();
|
||||
recoveryPersistence = { status: "checkpoint_ready", persistence: "browser_client_storage" };
|
||||
} catch (error) {
|
||||
recoveryPersistence = { status: "blocked", code: "runtime_checkpoint_create_failed", reason: String(error?.message ?? error) };
|
||||
}
|
||||
}
|
||||
if (this.xyzacDeviceIo) this.xyzacDeviceIo.command("INTERLOCK_RESET", input.payload ?? {}, input.operation_id);
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, active: input.payload?.active === true, task: this.task.export(), device_io: this.xyzacIo.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, active: input.payload?.active === true, task: this.task.export(), device_io: this.xyzacIo.export(), ...(recoveryCheckpoint ? { recovery_checkpoint: recoveryCheckpoint } : {}), recovery_persistence: recoveryPersistence, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
if (input.payload?.active !== true) return finish(this.error(input, "power_state", "power_active_required"), "failed");
|
||||
const result = this.xyzacIo ? this.xyzacIo.command("POWER", input.payload ?? {}) : this.devices.command("IO_RECOVER", input.payload ?? {});
|
||||
@@ -1210,12 +1377,25 @@ export class SimSession {
|
||||
if (kinematics.status === "blocked") return finish(this.envelope("ERROR", input, { status: "blocked", code: kinematics.code, kinematics: this.authoritative.export() }), "failed");
|
||||
}
|
||||
const loadResult = startupLoaded ? 0 : call(this.core, "sim_load_workspace", rootPath);
|
||||
let powerOffRecovery = null;
|
||||
if (loadResult === 0 && input.payload?.recovery_checkpoint && this.xyzacProgram) {
|
||||
const restored = this.restorePersistedPowerOff(input.payload.recovery_checkpoint, input.seq);
|
||||
if (restored.status !== "verified") return finish(this.envelope("ERROR", input, {
|
||||
status: "blocked",
|
||||
code: restored.code ?? "runtime_checkpoint_restore_failed",
|
||||
message_key: restored.reason ?? restored.code ?? "runtime_checkpoint_restore_failed",
|
||||
reason: restored.reason ?? restored.code ?? "runtime_checkpoint_restore_failed",
|
||||
recovery: restored,
|
||||
}), "failed");
|
||||
powerOffRecovery = restored;
|
||||
}
|
||||
return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, {
|
||||
status: "verified",
|
||||
command: input.kind,
|
||||
motion_authority: this.motionAuthority,
|
||||
state_hash: this.task.hash(),
|
||||
task: this.task.export(),
|
||||
...(powerOffRecovery ? { power_off_recovery: powerOffRecovery } : {}),
|
||||
...(this.configurationState?.schema === "configuration-startup-runtime.v1" ? { configuration: this.configurationState } : {}),
|
||||
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
|
||||
}
|
||||
|
||||
62
web/worker/test_xyzac_runtime_followups.mjs
Normal file
62
web/worker/test_xyzac_runtime_followups.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
||||
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
|
||||
|
||||
const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
||||
const PROGRAM = "G21 G90\nG0 X0 Y0 Z10 A0 C0\nG1 X20 Y5 Z5 A30 C45 F100\nM2\n";
|
||||
const fetchImpl = async (url) => {
|
||||
try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; }
|
||||
catch { return { ok: false, text: async () => "" }; }
|
||||
};
|
||||
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
||||
const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
|
||||
|
||||
async function open(sessionId) {
|
||||
const core = await createModule({ wasmBinary });
|
||||
const session = new SimSession(core, { sessionId });
|
||||
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: sessionId };
|
||||
let seq = 0;
|
||||
const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `${sessionId}-${seq}`, payload });
|
||||
send("HELLO"); send("SESSION_ATTACH");
|
||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified");
|
||||
return { session, send };
|
||||
}
|
||||
|
||||
const first = await open("xyzac-followups-a");
|
||||
const blocked = first.send("MDI_EXECUTE", { block: "G0 X1" });
|
||||
assert.equal(blocked.kind, "ERROR");
|
||||
assert.equal(blocked.payload.code, "axis_gate");
|
||||
assert.equal(blocked.payload.reason, "xyzac_machine_off");
|
||||
assert.equal(blocked.payload.message_key, "xyzac_machine_off");
|
||||
first.send("POWER", { active: true });
|
||||
first.send("HOME", { all: true });
|
||||
first.send("PROGRAM_LOAD", { source: PROGRAM, path: "followups.ngc" });
|
||||
first.send("PROGRAM_RUN");
|
||||
const compact = first.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
assert.equal(compact.payload.snapshot_mode, "motion_delta");
|
||||
assert.equal(compact.payload.panel_snapshot, undefined);
|
||||
assert.ok(compact.payload.motion_sample?.pose);
|
||||
const full = first.send("SIM_STEP", { cycles: 1 });
|
||||
const compactBytes = Buffer.byteLength(JSON.stringify(compact));
|
||||
const fullBytes = Buffer.byteLength(JSON.stringify(full));
|
||||
assert.ok(compactBytes < fullBytes / 2, `${compactBytes} >= ${fullBytes / 2}`);
|
||||
const off = first.send("POWER", { active: false });
|
||||
assert.equal(off.payload.recovery_checkpoint.schema, "xyzac-power-off-checkpoint.v1");
|
||||
assert.equal(off.payload.recovery_checkpoint.state.task.state, "STOPPED");
|
||||
assert.ok(Buffer.byteLength(JSON.stringify(off.payload.recovery_checkpoint)) < 512 * 1024);
|
||||
|
||||
const second = await open("xyzac-followups-b");
|
||||
const restored = second.send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle), recovery_checkpoint: off.payload.recovery_checkpoint });
|
||||
assert.equal(restored.payload.power_off_recovery.status, "verified");
|
||||
assert.equal(restored.payload.task.state, "STOPPED");
|
||||
assert.equal(restored.payload.panel_snapshot.program.program_status, "loaded");
|
||||
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-runtime-followups.v1",
|
||||
status: "verified",
|
||||
motion_delta: { compact_bytes: compactBytes, full_bytes: fullBytes, reduction_ratio: Number((1 - compactBytes / fullBytes).toFixed(3)) },
|
||||
errors: { code: blocked.payload.code, reason: blocked.payload.reason, message_key: blocked.payload.message_key },
|
||||
power_off_persistence: { schema: off.payload.recovery_checkpoint.schema, bytes: Buffer.byteLength(JSON.stringify(off.payload.recovery_checkpoint)), restored: restored.payload.power_off_recovery.status },
|
||||
}));
|
||||
@@ -7,6 +7,10 @@ const QUEUE_HEADER_BYTES = 32;
|
||||
const STATE_POSITION_BYTES = 72;
|
||||
const MOTION_QUEUE_SCHEMA = "sim_motion_queue_state.v1";
|
||||
const DEFAULT_CONTROLS = Object.freeze({ profile: 0, lookahead: 1, acceleration_limit: 1e9, jerk_limit: 1e12, paused: false });
|
||||
const MOTION_ERROR_REASONS = Object.freeze({
|
||||
[-600]: "wasm_motion_invalid_argument", [-601]: "wasm_motion_nonfinite_value", [-602]: "wasm_motion_limit_range",
|
||||
[-603]: "wasm_motion_invalid_state", [-604]: "wasm_motion_queue_full", [-605]: "wasm_motion_paused",
|
||||
});
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const fn = core[`_${name}`] ?? core[name];
|
||||
@@ -101,7 +105,7 @@ export class WasmMotionQueue {
|
||||
// Keep the WASM switchkins mode in the tag field; the C queue assigns id.
|
||||
ints[this.command / 4 + SEGMENT_INT_OFFSET + 3] = Number(segment.kinematics_mode ?? segment.tag ?? 0);
|
||||
const result = call(this.core, "sim_motion_enqueue", "number", ["number", "number", "number"], [this.queue, this.command, 0]);
|
||||
return result === 0 ? { status: "verified", source_line: segment.source_line ?? null } : { status: "error", code: `wasm_motion_enqueue_${result}` };
|
||||
return result === 0 ? { status: "verified", source_line: segment.source_line ?? null } : { status: "error", code: `wasm_motion_enqueue_${result}`, reason: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, message_key: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, source: "linuxcnc-wasm-motion" };
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -183,7 +187,7 @@ export class WasmMotionQueue {
|
||||
if (!this.available) return { status: "error", code: "wasm_motion_step_-605" };
|
||||
if (this.controls.paused) return { status: "error", code: "wasm_motion_step_-605" };
|
||||
const result = call(this.core, "sim_motion_step", "number", ["number"], [this.queue]);
|
||||
if (result !== 0) return { status: "error", code: `wasm_motion_step_${result}` };
|
||||
if (result !== 0) return { status: "error", code: `wasm_motion_step_${result}`, reason: MOTION_ERROR_REASONS[result] ?? `wasm_motion_step_${result}`, message_key: MOTION_ERROR_REASONS[result] ?? `wasm_motion_step_${result}`, source: "linuxcnc-wasm-motion" };
|
||||
return this.read();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ const SEGMENT_BYTES = 216;
|
||||
const POSE_BYTES = 72;
|
||||
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
|
||||
const DEFAULT_RAPID_VELOCITY = 1000;
|
||||
const PROGRAM_ERROR_REASONS = Object.freeze({
|
||||
[-1100]: "wasm_program_invalid_argument", [-1101]: "wasm_program_range_error", [-1102]: "wasm_program_invalid_state",
|
||||
[-1103]: "wasm_program_control_error", [-1104]: "wasm_program_interpreter_error", [-1105]: "wasm_program_motion_error",
|
||||
});
|
||||
|
||||
function programError(errorPrefix, result) {
|
||||
const reason = PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`;
|
||||
return { status: "error", code: `wasm_${errorPrefix}_${result}`, result, reason, message_key: reason, source: "linuxcnc-wasm-sim_program" };
|
||||
}
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const fn = core?.[`_${name}`] ?? core?.[name];
|
||||
@@ -69,7 +78,7 @@ export class WasmProgramRuntime {
|
||||
invoke(name, args = [], errorPrefix = name) {
|
||||
if (!this.available) return { status: "blocked", code: "wasm_program_unavailable" };
|
||||
const result = call(this.core, name, "number", ["number", ...args.map(() => "number")], [this.pointer, ...args]);
|
||||
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${errorPrefix}_${result}`, result };
|
||||
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${errorPrefix}_${result}`, result, reason: PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`, message_key: PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`, source: "linuxcnc-wasm-sim_program" };
|
||||
}
|
||||
|
||||
load(source, path = null) {
|
||||
@@ -79,7 +88,7 @@ export class WasmProgramRuntime {
|
||||
this.core.HEAPU8.set(bytes, pointer);
|
||||
try {
|
||||
const result = call(this.core, "sim_program_load", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]);
|
||||
if (result !== 0) return { status: "error", code: `wasm_program_load_${result}` };
|
||||
if (result !== 0) return programError("program_load", result);
|
||||
const velocityResult = call(this.core, "sim_program_set_rapid_velocity", "number", ["number", "number"], [this.pointer, this.rapidVelocity]);
|
||||
if (velocityResult !== 0) return { status: "error", code: `wasm_program_rapid_velocity_${velocityResult}` };
|
||||
this.source = source;
|
||||
@@ -191,7 +200,7 @@ export class WasmProgramRuntime {
|
||||
this.core.HEAPU8.set(bytes, pointer);
|
||||
try {
|
||||
const result = call(this.core, "sim_program_execute_block", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]);
|
||||
if (result !== 0) return { status: "error", code: `wasm_program_execute_${result}` };
|
||||
if (result !== 0) return programError("program_execute", result);
|
||||
return { status: "verified", program: this.snapshot(), motion: this.readMotion() };
|
||||
} finally { this.core._free(pointer); }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
|
||||
const DEVICE_SCHEMA = "wasm_xyzac_device_io.v1";
|
||||
const DEVICE_ERROR_REASONS = Object.freeze({
|
||||
[-1000]: "wasm_device_invalid_argument", [-1001]: "wasm_device_invalid_state", [-1002]: "wasm_device_range_error",
|
||||
[-1003]: "wasm_device_estop_active", [-1004]: "wasm_device_interlock_open", [-1005]: "wasm_device_fault_active",
|
||||
[-1006]: "wasm_device_ack_missing", [-1007]: "wasm_device_ack_timeout", [-1008]: "wasm_device_ack_invalid",
|
||||
});
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const fn = core?.[`_${name}`] ?? core?.[name];
|
||||
@@ -52,7 +57,8 @@ export class WasmXyzacDeviceIo {
|
||||
invoke(name, args = []) {
|
||||
if (!this.available) return { status: "blocked", code: "wasm_xyzac_device_io_unavailable" };
|
||||
const result = call(this.core, name, "number", ["number", ...args.map(() => "number")], [this.pointer, ...args]);
|
||||
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${name}_${result}`, result };
|
||||
const reason = DEVICE_ERROR_REASONS[result] ?? `wasm_${name}_${result}`;
|
||||
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${name}_${result}`, result, reason, message_key: reason, source: "linuxcnc-wasm-device-io" };
|
||||
}
|
||||
|
||||
canonical() {
|
||||
|
||||
@@ -4,6 +4,13 @@ const IO_SCHEMA = "wasm_xyzac_io.v1";
|
||||
const HAL_SCHEMA = "wasm_xyzac_hal.v1";
|
||||
const POSE_BYTES = 72;
|
||||
const HAL_WRITE_ALLOWLIST = new Set(["motion.probe-input", "iocontrol.0.tool-prepared", "iocontrol.0.tool-changed", "spindle.0.speed-in", "spindle.0.at-speed"]);
|
||||
const IO_ERROR_REASONS = Object.freeze({
|
||||
[-900]: "wasm_io_invalid_argument", [-901]: "wasm_io_range_error", [-902]: "wasm_io_invalid_state",
|
||||
[-903]: "wasm_io_power_required", [-904]: "wasm_io_estop_active", [-905]: "wasm_io_fault_active",
|
||||
[-906]: "wasm_io_limit_active", [-907]: "wasm_io_nonfinite_value", [-908]: "wasm_io_hal_pin_error",
|
||||
[-909]: "wasm_io_capacity_exceeded", [-910]: "wasm_io_busy", [-911]: "wasm_io_probe_error",
|
||||
[-912]: "wasm_io_timeout", [-913]: "wasm_io_recovery_required",
|
||||
});
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const fn = core?.[`_${name}`] ?? core?.[name];
|
||||
@@ -59,7 +66,7 @@ export class WasmXyzacIo {
|
||||
if (!this.available) return { status: "blocked", code: "wasm_xyzac_io_unavailable" };
|
||||
const result = call(this.core, name, "number", ["number", ...args.map(() => "number")], [this.pointer, ...args]);
|
||||
this.lastError = result;
|
||||
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${name}_${result}`, result };
|
||||
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${name}_${result}`, result, reason: IO_ERROR_REASONS[result] ?? `wasm_${name}_${result}`, message_key: IO_ERROR_REASONS[result] ?? `wasm_${name}_${result}`, source: "linuxcnc-wasm-io" };
|
||||
}
|
||||
|
||||
canonical() {
|
||||
|
||||
Reference in New Issue
Block a user