Complete 3axis tutorial native and Web/WASM parity tools

This commit is contained in:
2026-08-24 14:01:30 +08:00
parent 938654b19d
commit 7e90142b63
147 changed files with 17639 additions and 557 deletions

View File

@@ -21,6 +21,9 @@ import { createTrace, validateTrace } from "./trace-export.mjs";
import { ConfigurationBatchRuntime } from "../workspace/configuration-batch.mjs";
import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
import { axisState } from "./axis-state.mjs";
import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
import { WasmHomeLimit } from "./wasm-home-limit.mjs";
import { axisGate } from "../app/axis-contract.mjs";
const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
@@ -40,6 +43,13 @@ const OPERATION_KINDS = new Set([
"REMAP_REGISTER", "REMAP_CALL", "VCP_REGISTER", "VCP_UPDATE", "FILTER_EXEC",
]);
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", HOME: "home", SET_OVERRIDE: "override",
ESTOP: "estop", IO_RECOVER: "recover",
});
function text(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
@@ -90,10 +100,14 @@ export class SimSession {
this.python = new PythonBridge();
this.program = new ProgramRunner(core);
this.authoritative = new AuthoritativeRuntime();
this.wasmMotion = new WasmMotionQueue(core);
this.wasmHomeLimit = new WasmHomeLimit(core);
this.motionAuthority = "js";
this.task = new SimTask(core, { contextId, sessionId: sessionId || null });
this.scheduler = new SimScheduler(core);
this.configurationState = null;
this.halState = null;
this.previewState = null;
call(core, "sim_protocol_reset");
if (typeof core.ccall === "function") core.ccall("sim_event_reset", null, [], []);
}
@@ -102,6 +116,7 @@ export class SimSession {
const statePayload = payload && typeof payload === "object" ? payload : {};
const program = statePayload.program ?? (this.program.loaded ? this.program.snapshot() : {});
const task = statePayload.task ?? this.task.export();
const panelSnapshot = this.panelSnapshot({ task, program, input, tick });
return {
schema_version: PROTOCOL_SCHEMA,
kind,
@@ -110,10 +125,70 @@ export class SimSession {
seq: input.seq,
...(text(input.operation_id) ? { operation_id: input.operation_id } : {}),
simulation_tick: tick,
payload: { ...statePayload, axis_state: axisState({ task, program, action: input.kind, payload: input.payload ?? {} }) },
payload: {
...statePayload,
panel_snapshot: panelSnapshot,
motion_authority: statePayload.motion_authority ?? this.motionAuthority,
task_authority: statePayload.task_authority ?? this.task.authority(),
wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read(),
axis_state: axisState({ task, program, action: input.kind, payload: input.payload ?? {} }),
},
};
}
panelSnapshot({ task = this.task.export(), program = this.program.loaded ? this.program.snapshot() : null, input = {}, tick = this.tick } = {}) {
const motion = this.motion.export();
const wasmMotion = this.wasmMotion.read();
const homeLimit = this.wasmHomeLimit.read();
const devices = this.devices.export();
const deviceIo = this.deviceIo.export();
const halRuntime = this.hal.export();
const authoritative = this.authoritative.export();
const axis = axisState({ task, program: program ?? {}, action: input.kind, payload: input.payload ?? {} });
const gateInput = { task, program: program ?? {}, motion, devices, taskfile: Boolean(this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
const gateActions = ["load", "run", "step", "pause", "resume", "abort", "restart", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "reset", "spindle", "coolant"];
const gates = Object.fromEntries(gateActions.map((action) => [action, axisGate(action, gateInput)]));
const position = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
? wasmMotion.state.position
: authoritative.kinematics.pose;
const joints = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
? [position.x, position.y, position.z]
: authoritative.kinematics.joints;
const preview = this.previewState?.status === "verified" ? this.previewState : null;
const visual = {
schema: "axis-visual.v1",
pose: { x: position.x, y: position.y, z: position.z },
joints: Array.isArray(joints) ? [...joints] : [],
toolpath: (preview?.segments ?? []).map((segment, index) => ({ id: index + 1, ...segment })),
active_segment_id: wasmMotion.status === "verified" ? wasmMotion.state.active_id : authoritative.planner.active_id,
source: "wasm_canonical_motion",
};
const unsigned = {
schema: "axis-panel-snapshot.v1",
context_id: this.contextId,
session_id: this.sessionId,
simulation_tick: tick,
task,
program,
motion,
coordinates: { machine: [position.x, position.y, position.z], relative: [position.x, position.y, position.z], commanded: [position.x, position.y, position.z] },
overrides: { feed: motion.feed_override, spindle: [motion.spindle_override] },
wasm_motion: wasmMotion,
home_limit: homeLimit,
hal: { configuration: this.halState, runtime: halRuntime },
devices,
device_io: deviceIo,
configuration: this.configurationState,
axis_state: axis,
gates,
visual,
authoritative,
motion_authority: this.motionAuthority,
task_authority: this.task.authority(),
};
return { ...unsigned, state_hash: fnv1a64(unsigned) };
}
error(input, code, messageKey, retryable = false) {
return this.envelope("ERROR", input, {
status: "error",
@@ -135,6 +210,23 @@ export class SimSession {
return this.operations.get(input.operation_id) ?? null;
}
commandGate(input) {
const action = PANEL_GATE_ACTIONS[input.kind];
if (!action) return { action: null, enabled: true, reason: "axis_internal_command" };
const task = this.task.export();
const program = this.program.loaded ? this.program.snapshot() : {};
const motion = this.motion.export();
const devices = this.devices.export();
return axisGate(action, {
task,
program,
motion,
devices,
taskfile: Boolean(this.program.loaded),
kinematicsIdentity: this.motionAuthority === "wasm",
});
}
stateForCheckpoint() {
const state = {
context_id: this.contextId,
@@ -153,6 +245,11 @@ export class SimSession {
configuration: this.configurationState,
hal: this.halState,
python: this.python.exportState(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.exportState(),
task_authority: this.task.authority(),
wasm_home_limit: this.wasmHomeLimit.exportState(),
preview: this.previewState,
task: this.task.export(),
scheduler: this.scheduler.export(),
authoritative: this.authoritative.export(),
@@ -182,6 +279,10 @@ export class SimSession {
task: this.task.export(),
scheduler: this.scheduler.export(),
authoritative: this.authoritative.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.exportState(),
task_authority: this.task.authority(),
wasm_home_limit: this.wasmHomeLimit.exportState(),
};
if (this.program.loaded) value.program = this.program.exportState();
return { ...value, state_hash: fnv1a64(value) };
@@ -221,7 +322,17 @@ export class SimSession {
if (trace.state?.task) this.task.restore(trace.state.task);
if (trace.state?.scheduler) this.scheduler.restore(trace.state.scheduler);
if (trace.state?.authoritative) this.authoritative.restore(trace.state.authoritative);
if (trace.state?.motion_authority) this.motionAuthority = trace.state.motion_authority;
if (trace.state?.wasm_motion) this.wasmMotion.restoreState(trace.state.wasm_motion);
if (trace.state?.wasm_home_limit) this.wasmHomeLimit.restoreState(trace.state.wasm_home_limit);
if (trace.state?.motion) this.motion.restore(trace.state.motion);
if (trace.state?.program) {
this.program = new ProgramRunner(this.core);
this.program.restoreState(trace.state.program);
}
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 (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);
@@ -235,6 +346,10 @@ export class SimSession {
}
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
const replayMotionAuthority = checkpoint?.state?.motion_authority ?? this.motionAuthority;
if (replayMotionAuthority === "wasm" && !checkpoint?.state?.wasm_motion) {
return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ reason: "wasm_motion_checkpoint_missing" }] };
}
const replayDevices = new VirtualDevices({ state: checkpoint?.state?.devices });
const replayMotion = new MotionState();
if (checkpoint?.state?.motion) replayMotion.restore(checkpoint.state.motion);
@@ -256,6 +371,20 @@ export class SimSession {
if (checkpoint?.state?.python) replayPython.restoreState(checkpoint.state.python);
const replayAuthoritative = new AuthoritativeRuntime();
if (checkpoint?.state?.authoritative) replayAuthoritative.restore(checkpoint.state.authoritative);
let replayWasmMotion = null;
let replayHomeLimit = null;
try {
if (replayMotionAuthority === "wasm") {
replayWasmMotion = new WasmMotionQueue(this.core);
replayWasmMotion.restoreState(checkpoint.state.wasm_motion);
if (checkpoint.state.wasm_home_limit) {
replayHomeLimit = new WasmHomeLimit(this.core);
replayHomeLimit.restoreState(checkpoint.state.wasm_home_limit);
}
}
} 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) }] };
}
const result = recoverSimulation({
checkpoint,
contextId: this.contextId,
@@ -263,7 +392,11 @@ export class SimSession {
reconcile,
apply: (state, event) => {
if (DEVICE_COMMANDS.has(event.kind)) replayDevices.applyEvent(event.kind, event.payload);
if (event.kind === "HOME") replayMotion.home(event.payload?.axis ?? null);
if (event.kind === "HOME") {
replayMotion.home(event.payload?.axis ?? null);
const home = replayHomeLimit?.home(event.seq);
if (home?.status === "error") throw new Error(home.code);
}
if (event.kind === "JOG_START") replayMotion.startJog(event.payload);
if (event.kind === "JOG_STOP") replayMotion.stopJog();
if (event.kind === "SET_OVERRIDE") replayMotion.setOverride(event.payload?.feed ?? event.payload?.feed_override, event.payload?.spindle ?? event.payload?.spindle_override);
@@ -273,23 +406,61 @@ export class SimSession {
if (event.kind === "IO_RECOVER") replayDeviceIo.interlockReset();
if (event.kind === "SIM_STEP") replayMotion.step(event.tick);
if (event.kind === "PROGRAM_LOAD") replayProgram.load(event.payload?.source ?? event.payload?.text ?? event.payload?.program_source, event.payload?.path ?? null);
if (event.kind === "PROGRAM_RUN") replayProgram.start();
if (event.kind === "PROGRAM_PAUSE") replayProgram.pause();
if (event.kind === "PROGRAM_RESUME") replayProgram.resume();
if (event.kind === "PROGRAM_ABORT") replayProgram.abort();
if (event.kind === "PROGRAM_RESTART") replayProgram.restart();
if (event.kind === "PROGRAM_RUN") { replayProgram.start(); replayWasmMotion?.setPaused(false); }
if (event.kind === "PROGRAM_PAUSE") { replayProgram.pause(); replayWasmMotion?.setPaused(true); }
if (event.kind === "PROGRAM_RESUME") { replayProgram.resume(); replayWasmMotion?.setPaused(false); }
if (event.kind === "PROGRAM_ABORT") { replayProgram.abort(); replayWasmMotion?.abort(); }
if (event.kind === "PROGRAM_RESTART") { replayProgram.restart(); replayWasmMotion?.reset(); }
if (event.kind === "SIM_STEP" && replayProgram.loaded) {
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
for (let replayTick = replayAuthoritative.state.tick + 1; replayTick <= event.tick; replayTick += 1) {
replayProgram.tick(1);
const synced = replayAuthoritative.syncProgram(replayProgram.snapshot());
if (synced.status === "blocked") throw new Error(synced.code);
const stepped = replayAuthoritative.step(replayTick);
if (stepped.status === "error") throw new Error(stepped.code);
// PROGRAM_LOAD resets the planner clock, while the session clock is
// monotonic. Recovery must start this replay at the checkpoint tick
// or one logical step would consume the whole historical interval.
if (replayMotionAuthority === "wasm") {
if (!replayWasmMotion) throw new Error("wasm_motion_replay_unavailable");
const queueTick = replayWasmMotion.read().state.tick;
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
const cycles = Number.isSafeInteger(event.payload?.cycles) && event.payload.cycles >= 0 ? event.payload.cycles : 1;
for (let cycle = 0; cycle < cycles; cycle += 1) {
const replayTick = queueTick + cycle + 1;
const steps = replayProgram.tick(1);
for (const step of steps) {
if (!step.motion) continue;
const linearNoop = step.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => step.motion.start[axis] === step.motion.end[axis]);
if (!linearNoop) {
const enqueue = replayWasmMotion.enqueue(step.motion);
if (enqueue.status === "error") throw new Error(enqueue.code);
}
}
const stepped = replayWasmMotion.step();
if (stepped.status === "error" && stepped.code !== "wasm_motion_step_-605") throw new Error(stepped.code);
if (stepped.status === "verified") {
const projected = replayMotion.applyWasmPosition(stepped.state.position, stepped.state.velocity, replayTick);
if (projected.status === "error") throw new Error(projected.code);
if (replayHomeLimit?.read().state.homed) {
const checked = replayHomeLimit.check([stepped.state.position.x, stepped.state.position.y, stepped.state.position.z]);
if (checked.status === "error") throw new Error(checked.code);
}
}
}
} else {
if (Number.isSafeInteger(state.tick) && replayAuthoritative.state.tick < state.tick) replayAuthoritative.state.tick = state.tick;
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
for (let replayTick = replayAuthoritative.state.tick + 1; replayTick <= event.tick; replayTick += 1) {
replayProgram.tick(1);
const synced = replayAuthoritative.syncProgram(replayProgram.snapshot());
if (synced.status === "blocked") throw new Error(synced.code);
const stepped = replayAuthoritative.step(replayTick);
if (stepped.status === "error") throw new Error(stepped.code);
}
}
} else if (event.kind === "SIM_STEP") {
const stepped = replayAuthoritative.step(event.tick);
if (stepped.status === "error") throw new Error(stepped.code);
if (replayMotionAuthority === "wasm") {
const stepped = replayWasmMotion?.step();
if (stepped?.status === "error" && stepped.code !== "wasm_motion_step_-605") throw new Error(stepped.code);
} else {
const stepped = replayAuthoritative.step(event.tick);
if (stepped.status === "error") throw new Error(stepped.code);
}
}
if (event.kind === "PLC_CONFIG") replayPlc.configure(event.payload);
if (event.kind === "PLC_SET") replayPlc.setRegister(event.payload?.register, event.payload?.value);
@@ -352,7 +523,7 @@ export class SimSession {
const programStatus = replayProgram.status === "completed" ? "completed" : replayProgram.status === "running" ? "running" : replayProgram.status === "paused" ? "paused" : replayProgram.status === "aborted" ? "aborted" : "loaded";
task.current_line = replayProgram.currentLine;
task.program_status = programStatus;
if (programStatus === "completed" && task.state === "RUNNING") task.state = "STOPPED";
if (programStatus === "completed" && task.state === "RUNNING" && (replayMotionAuthority !== "wasm" || replayWasmMotion?.read().state.queue_depth === 0)) task.state = "STOPPED";
}
const schedulerUnsigned = {
schema: "sim_scheduler.v1",
@@ -365,11 +536,16 @@ export class SimSession {
last_cycles: event.kind === "SIM_STEP" && Number.isSafeInteger(event.payload?.cycles) ? event.payload.cycles : (state.scheduler?.last_cycles ?? 0),
state_hash: fnv1a64(schedulerUnsigned),
};
return { ...state, tick: event.tick, task: { ...task, state_hash: fnv1a64(task) }, scheduler, devices: replayDevices.export(), motion: replayMotion.export(), authoritative: replayAuthoritative.export(), plc: replayPlc.export(), hal_runtime: replayHal.export(), device_io: replayDeviceIo.export(), configuration_batch: replayConfigurationBatch.export(), io_ack: replayIoAck.export(), callbacks: replayCallbacks.export(), python: replayPython.exportState(), ...(replayProgram.loaded ? { program: replayProgram.exportState() } : {}) };
return { ...state, tick: event.tick, motion_authority: replayMotionAuthority, task_authority: replayMotionAuthority === "wasm" ? "wasm_task_canonical" : "js_compat", task: { ...task, state_hash: fnv1a64(task) }, scheduler, devices: replayDevices.export(), motion: replayMotion.export(), ...(replayWasmMotion ? { wasm_motion: replayWasmMotion.exportState() } : {}), ...(replayHomeLimit ? { wasm_home_limit: replayHomeLimit.exportState() } : {}), authoritative: replayAuthoritative.export(), plc: replayPlc.export(), hal_runtime: replayHal.export(), device_io: replayDeviceIo.export(), configuration_batch: replayConfigurationBatch.export(), io_ack: replayIoAck.export(), callbacks: replayCallbacks.export(), python: replayPython.exportState(), ...(replayProgram.loaded ? { program: replayProgram.exportState() } : {}) };
},
});
replayWasmMotion?.destroy();
replayHomeLimit?.destroy();
if (result.status === "recovered" || result.status === "aborted") {
this.tick = result.simulation_tick;
this.motionAuthority = result.state?.motion_authority ?? checkpoint?.state?.motion_authority ?? this.motionAuthority;
if (this.motionAuthority === "wasm" && result.state?.wasm_motion) this.wasmMotion.restoreState(result.state.wasm_motion);
if (this.motionAuthority === "wasm" && result.state?.wasm_home_limit) this.wasmHomeLimit.restoreState(result.state.wasm_home_limit);
if (result.state?.task) this.task.restore(result.state.task);
if (result.state?.scheduler) this.scheduler.restore(result.state.scheduler);
this.authoritative = new AuthoritativeRuntime();
@@ -377,7 +553,11 @@ export class SimSession {
this.devices = new VirtualDevices({ state: result.state?.devices ?? checkpoint?.state?.devices });
this.motion = new MotionState();
if (result.state?.motion) this.motion.restore(result.state.motion);
this.authoritative.applyMotion(this.motion);
if (this.motionAuthority !== "wasm") this.authoritative.applyMotion(this.motion);
if (this.motionAuthority === "wasm") {
const wasmMotion = this.wasmMotion.read();
if (wasmMotion.status !== "verified" || !result.state?.motion || fnv1a64(result.state.motion.axes) !== fnv1a64(wasmMotion.state.position)) throw new Error("wasm_motion_recovery_projection_mismatch");
}
this.plc = new PlcRuntime();
if (result.state?.plc) this.plc.restore(result.state.plc);
this.hal = new HalRuntime();
@@ -392,6 +572,7 @@ export class SimSession {
if (result.state?.callbacks) this.callbacks.restore(result.state.callbacks);
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;
this.python = new PythonBridge();
if (result.state?.python) this.python.restoreState(result.state.python);
this.program = new ProgramRunner(this.core);
@@ -478,6 +659,28 @@ export class SimSession {
if (text(input.operation_id) && OPERATION_KINDS.has(input.kind)) {
this.operations.set(input.operation_id, { operation_id: input.operation_id, status: "executing", seq: input.seq, kind: input.kind, result_hash: null });
}
const gate = this.commandGate(input);
const handlerOwnsFault = (input.kind === "JOG_START" || input.kind === "JOG_STOP") && Boolean(this.motion.state.fault);
const restartAfterCompletion = input.kind === "PROGRAM_RESTART" && this.program.status === "completed";
if (!gate.enabled && !handlerOwnsFault && !restartAfterCompletion) {
if (text(input.operation_id)) this.operations.delete(input.operation_id);
const rejectedPayload = { command: input.kind, gate_reason: gate.reason, payload: input.payload ?? {} };
try {
this.timeline.appendEvent(input.seq, this.tick, "COMMAND_REJECTED", rejectedPayload);
const rejectedEvent = appendCoreEvent(this.core, input.seq, this.tick, "COMMAND_REJECTED", rejectedPayload);
if (rejectedEvent !== 0) return this.error(input, "event_log", "core_event_append_failed");
} catch (error) {
return this.error(input, "event_log", String(error?.message ?? error));
}
return this.envelope("ERROR", input, {
status: "blocked",
code: "axis_gate",
message_key: gate.reason,
gate_reason: gate.reason,
retryable: false,
operation_id: input.operation_id ?? null,
});
}
if (Number.isSafeInteger(input.simulation_tick) && input.simulation_tick >= this.tick) this.tick = input.simulation_tick;
const eventTick = input.kind === "SIM_STEP" && Number.isSafeInteger(input.payload?.cycles)
? this.tick + input.payload.cycles : this.tick;
@@ -560,6 +763,19 @@ export class SimSession {
}
try { this.task.transition("READY"); } catch (error) { return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed"); }
return finish(this.envelope("SESSION_READY", input, { state_hash: this.task.hash(), task: this.task.export(), subscriptions: [] }), "verified");
case "SET_MODE": {
const mode = String(input.payload?.mode ?? "").toUpperCase();
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
}
case "RESET": {
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "reset_while_program_active"), "failed");
const result = this.motion.resetFault();
if (result.status !== "verified") return finish(this.envelope("ERROR", input, { ...result, command: input.kind, task: this.task.export() }), "failed");
if (this.task.state.state === "FAULT" || this.task.state.state === "STOPPED") this.task.recover();
return finish(this.envelope("COMMAND_RESULT", input, { ...result, command: input.kind, task: this.task.export(), motion: this.motion.export(), operation_id: input.operation_id ?? null }));
}
case "LOAD_WORKSPACE": {
const rootPath = input.payload?.root_path;
if (!text(rootPath) || !rootPath.startsWith("/") || rootPath.includes("..")) return finish(this.error(input, "workspace_path", "invalid_workspace_path"), "failed");
@@ -588,7 +804,7 @@ export class SimSession {
startup: startupReport,
}), "failed");
}
this.configurationState = startupReport;
this.configurationState = startupReport;
this.created = true;
startupLoaded = true;
} else {
@@ -610,6 +826,11 @@ export class SimSession {
};
}
}
this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") ? "wasm" : "js";
if (configuration?.home_limits && this.motionAuthority === "wasm") {
this.wasmHomeLimit.destroy();
this.wasmHomeLimit = new WasmHomeLimit(this.core, { limits: configuration.home_limits });
}
if (input.payload?.hal) {
const preflight = preflightHalConfiguration(input.payload.hal);
if (preflight.status !== "ready") {
@@ -636,6 +857,7 @@ export class SimSession {
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(),
...(this.configurationState?.schema === "configuration-startup-runtime.v1" ? { configuration: this.configurationState } : {}),
@@ -648,19 +870,30 @@ export class SimSession {
let program_steps = [];
try {
this.scheduler.step(cycles, {
task: () => {
program_steps.push(...this.program.tick(1));
task: () => {
const steps = this.program.tick(1);
program_steps.push(...steps);
for (const step of steps) {
if (!step.motion) continue;
const linearNoop = step.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => step.motion.start[axis] === step.motion.end[axis]);
if (!linearNoop) {
const enqueue = this.wasmMotion.enqueue(step.motion);
if (enqueue.status === "error") throw new Error(enqueue.code);
}
}
},
tp: (nextTick) => {
if (this.program.loaded) {
if (this.motionAuthority !== "wasm" && this.program.loaded) {
const sync = this.authoritative.syncProgram(this.program.snapshot());
if (sync.status === "blocked") throw new Error(sync.code);
}
const planner = this.authoritative.step(nextTick);
if (planner.status === "error") throw new Error(planner.code);
if (this.motionAuthority !== "wasm") {
const planner = this.authoritative.step(nextTick);
if (planner.status === "error") throw new Error(planner.code);
}
},
kinematics: () => {
if (!["trivkins", "corexy", "tripod", "lineardelta", "rotarydelta"].includes(this.authoritative.state.kinematics.type)) throw new Error("authoritative_kinematics_unsupported");
if (this.motionAuthority !== "wasm" && !["trivkins", "corexy", "tripod", "lineardelta", "rotarydelta"].includes(this.authoritative.state.kinematics.type)) throw new Error("authoritative_kinematics_unsupported");
},
motion: (nextTick) => {
const stepResult = call(this.core, "sim_step", 1);
@@ -668,8 +901,19 @@ export class SimSession {
this.tick = nextTick;
this.task.setTick(nextTick);
const motionState = this.motion.step(nextTick);
const wasmMotion = this.wasmMotion.step();
if (wasmMotion.status === "error" && wasmMotion.code !== "wasm_motion_step_-605") throw new Error(wasmMotion.code);
if (motionState.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
if (this.program.loaded) this.authoritative.applyMotion(this.motion);
if (this.motionAuthority === "wasm") {
if (wasmMotion.status === "verified") {
const position = this.motion.applyWasmPosition(wasmMotion.state.position, wasmMotion.state.velocity, nextTick);
if (position.status === "error") throw new Error(position.code);
}
} else if (this.program.loaded) this.authoritative.applyMotion(this.motion);
if (this.motionAuthority === "wasm" && this.wasmHomeLimit.read().state.homed && !motionState.fault) {
const homeCheck = this.wasmHomeLimit.check(this.motion.state.joints);
if (homeCheck.status === "error") throw new Error(homeCheck.code);
}
const halState = this.hal.step(nextTick);
if (halState.status === "error") throw new Error(halState.code);
const deviceIoState = this.deviceIo.step(nextTick);
@@ -692,12 +936,12 @@ export class SimSession {
if (program) {
const status = program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status;
this.task.setProgram(status, program.line);
if (status === "completed" && this.task.state.state === "RUNNING") this.task.transition("STOPPED");
if (status === "completed" && this.task.state.state === "RUNNING" && (this.motionAuthority !== "wasm" || this.wasmMotion.read().state.queue_depth === 0)) this.task.transition("STOPPED");
}
const snapshotState = { tick: this.tick, cycles, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.authoritative.export(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export() };
const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export() };
if (program) snapshotState.program = program;
const snapshot = this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, program_steps, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.authoritative.export(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), ...(program ? { program } : {}), state_hash: snapshot.state_hash, snapshot }));
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, program_steps, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), ...(program ? { program } : {}), state_hash: snapshot.state_hash, snapshot }));
}
case "PROGRAM_LOAD": {
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed");
@@ -705,11 +949,20 @@ export class SimSession {
const result = this.program.load(source, input.payload?.path ?? null);
if (result.status === "verified") {
this.authoritative.reset();
this.wasmMotion.reset();
this.wasmMotion.setPaused(false);
this.motion.applyWasmPosition(this.wasmMotion.read().state.position, 0, this.tick);
this.task.setProgram("loaded", 0);
const preview = this.program.preview();
result.preview = preview.preview ?? { status: "error", code: preview.code ?? "program_preview_failed" };
this.previewState = result.preview;
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
motion: this.motion.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
@@ -719,12 +972,16 @@ export class SimSession {
if (result.status === "verified") {
this.task.setMode("AUTO");
if (this.task.state.state === "STOPPED") this.task.recover();
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(false);
this.task.transition("RUNNING");
this.task.setProgram("running", this.program.currentLine);
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
motion: this.motion.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
@@ -733,13 +990,25 @@ export class SimSession {
const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort";
const result = this.program[action]();
if (result.status === "verified") {
if (action === "pause") this.task.transition("PAUSED");
else if (action === "resume") this.task.transition("RUNNING");
else { this.authoritative.abort(); this.task.transition("STOPPED"); this.task.setProgram("aborted", this.program.currentLine); }
if (action === "pause") {
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(true);
this.task.transition("PAUSED");
} else if (action === "resume") {
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(false);
this.task.transition("RUNNING");
} else {
if (this.motionAuthority === "wasm") this.wasmMotion.abort();
else this.authoritative.abort();
this.task.transition("STOPPED");
this.task.setProgram("aborted", this.program.currentLine);
}
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
motion: this.motion.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
@@ -748,9 +1017,13 @@ export class SimSession {
const result = this.program.restart();
if (result.status === "verified") {
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();
this.task.setMode("AUTO");
this.authoritative.reset();
this.wasmMotion.reset();
this.wasmMotion.setPaused(false);
this.motion.applyWasmPosition(this.wasmMotion.read().state.position, 0, this.tick);
this.task.setProgram("loaded", 0);
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
@@ -767,14 +1040,26 @@ export class SimSession {
}
const result = this.program.step();
if (result.status === "verified") {
if (this.motionAuthority === "wasm" && result.motion) {
const linearNoop = result.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => result.motion.start[axis] === result.motion.end[axis]);
if (!linearNoop) {
const enqueue = this.wasmMotion.enqueue(result.motion);
if (enqueue.status === "error") return finish(this.error(input, "motion_queue", enqueue.code), "failed");
}
this.wasmMotion.setPaused(true);
}
this.task.setProgram(result.program_status === "completed" ? "completed" : "paused", result.line);
if (result.program_status === "completed") {
if (this.task.state.state === "RUNNING") this.task.transition("STOPPED");
const pending = this.motionAuthority === "wasm" ? this.wasmMotion.read().state.queue_depth > 0 : this.authoritative.state.planner.queue_depth > 0;
if (this.task.state.state === "RUNNING") this.task.transition(pending ? "PAUSED" : "STOPPED");
} else if (this.task.state.state === "RUNNING") this.task.transition("PAUSED");
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
motion_authority: this.motionAuthority,
wasm_motion: this.wasmMotion.read(),
motion: this.motion.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
@@ -803,16 +1088,21 @@ export class SimSession {
}
case "HOME": {
const result = this.motion.home(input.payload?.axis ?? null);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
if (result.status === "verified" && this.motionAuthority === "wasm") {
const home = this.wasmHomeLimit.home(input.seq);
if (home.status === "error") return finish(this.error(input, "home_limit", home.code), "failed");
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motion.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "JOG_START": case "JOG_STOP": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
let result;
if (input.kind === "JOG_START" && this.motionAuthority === "wasm" && !this.wasmHomeLimit.read().state.homed) return finish(this.error(input, "motion_state", "motion_unhomed"), "failed");
if (input.kind === "JOG_START") result = this.motion.startJog(input.payload);
else if (input.kind === "JOG_STOP") result = this.motion.stopJog();
else if (input.kind === "SET_OVERRIDE") result = this.motion.setOverride(input.payload?.feed ?? input.payload?.feed_override, input.payload?.spindle ?? input.payload?.spindle_override);
else result = this.motion.setFollowingError(input.payload?.value ?? input.payload?.following_error);
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, task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motion.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "SPINDLE_COMMAND": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":