Implement AXIS parity UI and execution baseline
This commit is contained in:
@@ -3,22 +3,40 @@ import { capabilities, validateLimit } from "./capabilities.mjs";
|
||||
import { mapCoreError } from "./error-map.mjs";
|
||||
import { createCheckpoint, recoverSimulation } from "../workspace/recovery.mjs";
|
||||
import { preflightConfiguration } from "../workspace/configuration.mjs";
|
||||
import { ConfigurationStartupStateMachine } from "../workspace/configuration.mjs";
|
||||
import { preflightHalConfiguration } from "../workspace/hal-configuration.mjs";
|
||||
import { DEVICE_COMMANDS, VirtualDevices } from "./virtual-devices.mjs";
|
||||
import { PythonBridge } from "./python-bridge.mjs";
|
||||
import { ProgramRunner } from "./program-runner.mjs";
|
||||
import { SimTask } from "./sim-task.mjs";
|
||||
import { SimScheduler, SCHEDULER_PHASES } from "./sim-scheduler.mjs";
|
||||
import { OperationLedger } from "./operation-ledger.mjs";
|
||||
import { MotionState } from "./motion-state.mjs";
|
||||
import { PlcRuntime } from "./plc-runtime.mjs";
|
||||
import { HalRuntime } from "./hal-runtime.mjs";
|
||||
import { DeviceIoRuntime } from "./device-io.mjs";
|
||||
import { IoAckTracker } from "./io-ack.mjs";
|
||||
import { PythonCallbacks } from "./python-callbacks.mjs";
|
||||
import { createTrace, validateTrace } from "./trace-export.mjs";
|
||||
import { ConfigurationBatchRuntime } from "../workspace/configuration-batch.mjs";
|
||||
import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
|
||||
|
||||
const PROTOCOL_SCHEMA = 1;
|
||||
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
||||
|
||||
const OPERATION_KINDS = new Set([
|
||||
"LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
||||
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
||||
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
|
||||
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP",
|
||||
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP",
|
||||
"HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "TOOL_PREPARE",
|
||||
"TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
|
||||
"SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER",
|
||||
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET",
|
||||
"ENCODER_SET", "COOLANT_COMMAND", "COOLANT_FAULT", "DOOR_SET", "INTERLOCK_SET", "INTERLOCK_RESET", "DEVICE_ESTOP", "DEVICE_ACK_ISSUE", "DEVICE_ACK", "DEVICE_ACK_ABORT",
|
||||
"CONFIGURATION_BATCH",
|
||||
"PYTHON_IMPORT", "PYTHON_CALL", "PYTHON_CAPABILITY",
|
||||
"PROGRAM_STEP", "PROGRAM_SEEK", "MDI_EXECUTE", "PLC_CONFIG", "PLC_SET", "PLC_TRIGGER", "FOLLOWING_ERROR", "IO_COMMAND", "IO_ACK", "IO_TIMEOUT",
|
||||
"REMAP_REGISTER", "REMAP_CALL", "VCP_REGISTER", "VCP_UPDATE", "FILTER_EXEC",
|
||||
]);
|
||||
|
||||
function text(value) {
|
||||
@@ -59,8 +77,20 @@ export class SimSession {
|
||||
this.created = false;
|
||||
this.timeline = new ReplayTimeline();
|
||||
this.operations = new Map();
|
||||
this.ledger = new OperationLedger();
|
||||
this.devices = new VirtualDevices();
|
||||
this.motion = new MotionState();
|
||||
this.plc = new PlcRuntime();
|
||||
this.hal = new HalRuntime();
|
||||
this.deviceIo = new DeviceIoRuntime();
|
||||
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: contextId });
|
||||
this.ioAck = new IoAckTracker();
|
||||
this.callbacks = new PythonCallbacks();
|
||||
this.python = new PythonBridge();
|
||||
this.program = new ProgramRunner(core);
|
||||
this.authoritative = new AuthoritativeRuntime();
|
||||
this.task = new SimTask(core, { contextId, sessionId: sessionId || null });
|
||||
this.scheduler = new SimScheduler(core);
|
||||
this.configurationState = null;
|
||||
this.halState = null;
|
||||
call(core, "sim_protocol_reset");
|
||||
@@ -102,17 +132,30 @@ export class SimSession {
|
||||
}
|
||||
|
||||
stateForCheckpoint() {
|
||||
return {
|
||||
const state = {
|
||||
context_id: this.contextId,
|
||||
session_id: this.sessionId,
|
||||
tick: this.tick,
|
||||
created: this.created,
|
||||
protocol_seq: this.timeline.events.cursor,
|
||||
devices: this.devices.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(),
|
||||
configuration: this.configurationState,
|
||||
hal: this.halState,
|
||||
python: this.python.exportState(),
|
||||
task: this.task.export(),
|
||||
scheduler: this.scheduler.export(),
|
||||
authoritative: this.authoritative.export(),
|
||||
ledger: this.ledger.export(),
|
||||
};
|
||||
if (this.program.loaded) state.program = this.program.exportState();
|
||||
return state;
|
||||
}
|
||||
|
||||
workspacePackageState() {
|
||||
@@ -122,10 +165,21 @@ export class SimSession {
|
||||
configuration: this.configurationState,
|
||||
hal: this.halState,
|
||||
devices: this.devices.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(),
|
||||
python: this.python.exportState(),
|
||||
tick: this.tick,
|
||||
event_cursor: this.timeline.events.cursor,
|
||||
task: this.task.export(),
|
||||
scheduler: this.scheduler.export(),
|
||||
authoritative: this.authoritative.export(),
|
||||
};
|
||||
if (this.program.loaded) value.program = this.program.exportState();
|
||||
return { ...value, state_hash: fnv1a64(value) };
|
||||
}
|
||||
|
||||
@@ -147,10 +201,57 @@ export class SimSession {
|
||||
});
|
||||
}
|
||||
|
||||
exportTrace() {
|
||||
const latest = this.timeline.snapshots.latest();
|
||||
if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) {
|
||||
this.timeline.checkpoint(this.timeline.events.cursor, this.timeline.events.tick, this.stateForCheckpoint(), latest?.snapshot_id ?? null);
|
||||
}
|
||||
return createTrace({ contextId: this.contextId, timeline: this.timeline, state: this.stateForCheckpoint(), task: this.task.export(), tick: this.tick });
|
||||
}
|
||||
|
||||
importTrace(trace) {
|
||||
const validated = validateTrace(trace, { contextId: this.contextId });
|
||||
this.timeline = validated.timeline;
|
||||
this.created = Boolean(trace.state?.created);
|
||||
if (trace.state?.ledger?.entries) this.ledger = new OperationLedger(trace.state.ledger.entries);
|
||||
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) this.motion.restore(trace.state.motion);
|
||||
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);
|
||||
if (trace.state?.configuration_batch) this.configurationBatch.restore(trace.state.configuration_batch);
|
||||
if (trace.state?.io_ack) this.ioAck.restore(trace.state.io_ack);
|
||||
if (trace.state?.callbacks) this.callbacks.restore(trace.state.callbacks);
|
||||
if (trace.state?.python) this.python.restoreState(trace.state.python);
|
||||
if (trace.state?.devices) this.devices = new VirtualDevices({ state: trace.state.devices });
|
||||
this.tick = trace.tick;
|
||||
return { status: "verified", trace_hash: trace.trace_hash, state_hash: fnv1a64(trace.state), seq: trace.seq, tick: trace.tick };
|
||||
}
|
||||
|
||||
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
|
||||
const replayDevices = new VirtualDevices({ state: checkpoint?.state?.devices });
|
||||
const replayMotion = new MotionState();
|
||||
if (checkpoint?.state?.motion) replayMotion.restore(checkpoint.state.motion);
|
||||
const replayPlc = new PlcRuntime();
|
||||
if (checkpoint?.state?.plc) replayPlc.restore(checkpoint.state.plc);
|
||||
const replayHal = new HalRuntime();
|
||||
if (checkpoint?.state?.hal_runtime) replayHal.restore(checkpoint.state.hal_runtime);
|
||||
const replayDeviceIo = new DeviceIoRuntime();
|
||||
if (checkpoint?.state?.device_io) replayDeviceIo.restore(checkpoint.state.device_io);
|
||||
const replayConfigurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId });
|
||||
if (checkpoint?.state?.configuration_batch) replayConfigurationBatch.restore(checkpoint.state.configuration_batch);
|
||||
const replayIoAck = new IoAckTracker();
|
||||
if (checkpoint?.state?.io_ack) replayIoAck.restore(checkpoint.state.io_ack);
|
||||
const replayCallbacks = new PythonCallbacks();
|
||||
if (checkpoint?.state?.callbacks) replayCallbacks.restore(checkpoint.state.callbacks);
|
||||
const replayProgram = new ProgramRunner(this.core);
|
||||
if (checkpoint?.state?.program) replayProgram.restoreState(checkpoint.state.program);
|
||||
const replayPython = new PythonBridge();
|
||||
if (checkpoint?.state?.python) replayPython.restoreState(checkpoint.state.python);
|
||||
const replayAuthoritative = new AuthoritativeRuntime();
|
||||
if (checkpoint?.state?.authoritative) replayAuthoritative.restore(checkpoint.state.authoritative);
|
||||
const result = recoverSimulation({
|
||||
checkpoint,
|
||||
contextId: this.contextId,
|
||||
@@ -158,19 +259,132 @@ 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 === "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);
|
||||
if (event.kind === "LIMIT_SET") replayMotion.setLimit(event.payload?.axis, event.payload?.active === true);
|
||||
if (event.kind === "ESTOP") replayMotion.estop(event.payload?.active === true);
|
||||
if (event.kind === "ESTOP") replayDeviceIo.estop(event.payload?.active === true);
|
||||
if (event.kind === "IO_RECOVER") replayDeviceIo.interlockReset();
|
||||
if (event.kind === "SIM_STEP") replayMotion.step(event.tick);
|
||||
if (event.kind === "SIM_STEP") replayAuthoritative.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 === "SIM_STEP" && replayProgram.loaded) {
|
||||
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
|
||||
replayProgram.tick(Number.isSafeInteger(event.payload?.cycles) ? event.payload.cycles : 1);
|
||||
replayAuthoritative.syncProgram(replayProgram.snapshot());
|
||||
}
|
||||
if (event.kind === "PLC_CONFIG") replayPlc.configure(event.payload);
|
||||
if (event.kind === "PLC_SET") replayPlc.setRegister(event.payload?.register, event.payload?.value);
|
||||
if (event.kind === "PLC_TRIGGER") replayPlc.trigger();
|
||||
if (event.kind === "SIM_STEP") for (let replayTick = replayPlc.state.tick + 1; replayTick <= event.tick; replayTick += 1) replayPlc.scan(replayTick);
|
||||
if (event.kind === "HAL_COMMAND" && event.payload?.action === "configure") replayHal.configure(event.payload);
|
||||
if (event.kind === "HAL_COMMAND" && event.payload?.action === "thread") replayHal.registerThread(event.payload?.name, event.payload?.period_ticks);
|
||||
if (event.kind === "HAL_COMMAND" && event.payload?.action === "component") replayHal.registerComponent(event.payload?.instance, event.payload?.type, event.payload?.thread);
|
||||
if (event.kind === "HAL_COMMAND" && event.payload?.action === "connect") replayHal.connect(event.payload?.source, event.payload?.target);
|
||||
if (event.kind === "HAL_COMMAND" && event.payload?.action === "failure") replayHal.setFailure(event.payload?.instance, event.payload?.failed === true);
|
||||
if (event.kind === "HAL_SET" && event.payload?.target === "pin") replayHal.setPin(event.payload?.name, event.payload?.value, event.payload?.type);
|
||||
if (event.kind === "HAL_SET" && event.payload?.target === "param") replayHal.setParam(event.payload?.name, event.payload?.value, event.payload?.type);
|
||||
if (event.kind === "SIM_STEP") replayHal.step(event.tick);
|
||||
if (event.kind === "ENCODER_SET") replayDeviceIo.encoderSet(event.payload?.channel, event.payload);
|
||||
if (event.kind === "COOLANT_COMMAND") replayDeviceIo.coolantCommand(event.payload);
|
||||
if (event.kind === "COOLANT_FAULT") replayDeviceIo.coolantFault(event.payload?.fault === true);
|
||||
if (event.kind === "DOOR_SET") replayDeviceIo.doorSet(event.payload?.open === true);
|
||||
if (event.kind === "INTERLOCK_SET") replayDeviceIo.interlockSet(event.payload?.input_ok === true);
|
||||
if (event.kind === "INTERLOCK_RESET") replayDeviceIo.interlockReset();
|
||||
if (event.kind === "DEVICE_ESTOP") replayDeviceIo.estop(event.payload?.active === true);
|
||||
if (event.kind === "DEVICE_ACK_ISSUE") replayDeviceIo.issueAck(event.operation_id ?? event.payload?.operation_id, event.payload?.timeout_ticks ?? 10);
|
||||
if (event.kind === "DEVICE_ACK") replayDeviceIo.ack(event.payload?.operation_id);
|
||||
if (event.kind === "DEVICE_ACK_ABORT") replayDeviceIo.abortAck(event.payload?.operation_id);
|
||||
if (event.kind === "SIM_STEP") replayDeviceIo.step(event.tick);
|
||||
if (event.kind === "CONFIGURATION_BATCH") replayConfigurationBatch.load(event.payload?.entries ?? [], { context_id: this.contextId });
|
||||
if (event.kind === "IO_COMMAND") replayIoAck.issue(event.payload?.operation_id ?? event.payload?.command_id, event.payload?.kind ?? "IO", event.tick, event.payload?.timeout_ticks ?? 10);
|
||||
if (event.kind === "IO_COMMAND") replayDeviceIo.issueAck(event.payload?.operation_id ?? event.payload?.command_id, event.payload?.timeout_ticks ?? 10);
|
||||
if (event.kind === "IO_ACK") replayIoAck.ack(event.payload?.operation_id, event.tick);
|
||||
if (event.kind === "IO_ACK") replayDeviceIo.ack(event.payload?.operation_id);
|
||||
if (event.kind === "IO_TIMEOUT") replayIoAck.step(event.tick);
|
||||
if (event.kind === "SIM_STEP") replayIoAck.step(event.tick);
|
||||
if (event.kind === "IO_TIMEOUT") replayDeviceIo.ackStep(event.tick);
|
||||
if (event.kind === "REMAP_REGISTER") replayCallbacks.registerRemap(event.payload?.name, event.payload);
|
||||
if (event.kind === "VCP_REGISTER") replayCallbacks.registerVcp(event.payload?.name, event.payload?.pins ?? []);
|
||||
if (event.kind === "FILTER_EXEC") replayCallbacks.filter(event.payload?.name, event.payload?.source ?? "", event.payload?.args ?? []);
|
||||
if (event.kind === "REMAP_CALL") replayCallbacks.invoke("remap", event.payload?.name, event.payload?.phase ?? "prolog", event.payload?.args ?? []);
|
||||
if (event.kind === "VCP_UPDATE") replayCallbacks.updateVcp(event.payload?.name, event.payload?.pin, event.payload?.value);
|
||||
if (event.kind === "PYTHON_IMPORT") replayPython.importModule(event.payload?.module);
|
||||
if (event.kind === "PYTHON_CALL") replayPython.invoke(event.payload?.module, event.payload?.symbol, event.payload?.args ?? []);
|
||||
if (event.kind === "PYTHON_CAPABILITY") replayPython.checkCapability(event.payload?.capability);
|
||||
return { ...state, tick: event.tick, devices: replayDevices.export(), python: replayPython.exportState() };
|
||||
if (event.kind === "SIM_STEP") replayCallbacks.setTick(event.tick);
|
||||
if (event.kind === "SIM_STEP") replayPython.setTick(event.tick);
|
||||
const task = { ...(state.task ?? this.task.export()), seq: event.seq, simulation_tick: event.tick };
|
||||
delete task.state_hash;
|
||||
if (event.kind === "ESTOP" && event.payload?.active === true) {
|
||||
task.state = "ESTOP";
|
||||
} else if (event.kind === "IO_RECOVER") {
|
||||
task.state = "READY";
|
||||
} else if (event.kind === "PROGRAM_PAUSE") {
|
||||
task.state = "PAUSED";
|
||||
task.program_status = "paused";
|
||||
} else if (event.kind === "PROGRAM_RESUME" || event.kind === "PROGRAM_RUN") {
|
||||
task.state = "RUNNING";
|
||||
task.program_status = "running";
|
||||
} else if (event.kind === "PROGRAM_ABORT" || event.kind === "ABORT") {
|
||||
task.state = "STOPPED";
|
||||
task.program_status = "aborted";
|
||||
}
|
||||
if (replayProgram.loaded) {
|
||||
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";
|
||||
}
|
||||
const schedulerUnsigned = {
|
||||
schema: "sim_scheduler.v1",
|
||||
budget: state.scheduler?.budget ?? 1000000,
|
||||
phase_trace: [...SCHEDULER_PHASES],
|
||||
tick: event.tick,
|
||||
};
|
||||
const scheduler = {
|
||||
...schedulerUnsigned,
|
||||
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() } : {}) };
|
||||
},
|
||||
});
|
||||
if (result.status === "recovered" || result.status === "aborted") {
|
||||
this.tick = result.simulation_tick;
|
||||
if (result.state?.task) this.task.restore(result.state.task);
|
||||
if (result.state?.scheduler) this.scheduler.restore(result.state.scheduler);
|
||||
this.authoritative = new AuthoritativeRuntime();
|
||||
if (result.state?.authoritative) this.authoritative.restore(result.state.authoritative);
|
||||
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);
|
||||
this.plc = new PlcRuntime();
|
||||
if (result.state?.plc) this.plc.restore(result.state.plc);
|
||||
this.hal = new HalRuntime();
|
||||
if (result.state?.hal_runtime) this.hal.restore(result.state.hal_runtime);
|
||||
this.deviceIo = new DeviceIoRuntime();
|
||||
if (result.state?.device_io) this.deviceIo.restore(result.state.device_io);
|
||||
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId });
|
||||
if (result.state?.configuration_batch) this.configurationBatch.restore(result.state.configuration_batch);
|
||||
this.ioAck = new IoAckTracker();
|
||||
if (result.state?.io_ack) this.ioAck.restore(result.state.io_ack);
|
||||
this.callbacks = new PythonCallbacks();
|
||||
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.python = new PythonBridge();
|
||||
if (result.state?.python) this.python.restoreState(result.state.python);
|
||||
this.program = new ProgramRunner(this.core);
|
||||
if (result.state?.program) this.program.restoreState(result.state.program);
|
||||
this.timeline = new ReplayTimeline(checkpoint.timeline);
|
||||
if (events?.schema === "replay-timeline.v1") {
|
||||
for (const event of events.events.events.slice(this.timeline.events.cursor)) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash);
|
||||
@@ -178,6 +392,11 @@ export class SimSession {
|
||||
for (const event of events) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash);
|
||||
}
|
||||
this.operations = new Map(result.operations.map((operation) => [operation.operation_id, operation]));
|
||||
this.ledger = new OperationLedger();
|
||||
for (const operation of result.operations) {
|
||||
const entry = { ...operation, phase: operation.phase ?? (operation.status === "verified" ? "committed" : operation.status), side_effect_count: operation.side_effect_count ?? (operation.status === "verified" ? 1 : 0) };
|
||||
try { this.ledger.importEntry(entry); } catch { /* legacy checkpoint entries remain in operations */ }
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -188,7 +407,10 @@ export class SimSession {
|
||||
if (input.context_id !== this.contextId) return ["protocol_context", "context_mismatch"];
|
||||
if (!text(input.kind) || !Number.isSafeInteger(input.seq) || input.seq <= 0) return ["protocol_argument", "invalid_sequence_or_kind"];
|
||||
if (this.sessionId && input.session_id !== this.sessionId) return ["protocol_session", "session_mismatch"];
|
||||
if (!this.sessionId && text(input.session_id)) this.sessionId = input.session_id;
|
||||
if (!this.sessionId && text(input.session_id)) {
|
||||
this.sessionId = input.session_id;
|
||||
this.task.bindSession(this.sessionId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -197,6 +419,37 @@ export class SimSession {
|
||||
if (invalid) return this.error(input ?? {}, invalid[0], invalid[1]);
|
||||
const result = call(this.core, "sim_protocol_accept", input.schema_version, input.seq, input.kind, input.operation_id ?? "", input.simulation_tick ?? this.tick);
|
||||
if (result !== 0) return this.coreError(input, result, "sim_protocol_accept");
|
||||
let ledgerPreparation;
|
||||
try {
|
||||
ledgerPreparation = this.ledger.prepare(input);
|
||||
if (ledgerPreparation.replay && ledgerPreparation.entry?.response) {
|
||||
const replayTick = Number.isSafeInteger(input.simulation_tick) && input.simulation_tick >= this.tick ? input.simulation_tick : this.tick;
|
||||
this.timeline.appendEvent(input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: ledgerPreparation.entry.seq });
|
||||
const replayEventResult = appendCoreEvent(this.core, input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: ledgerPreparation.entry.seq });
|
||||
if (replayEventResult !== 0) return this.error(input, "event_log", "core_event_append_failed");
|
||||
this.tick = replayTick;
|
||||
try { this.task.setCommand(input.seq, replayTick); } catch (error) { return this.error(input, "task_state", String(error?.message ?? error)); }
|
||||
return { ...ledgerPreparation.entry.response, replayed: true, seq: input.seq, ledger: this.ledger.reconcile(input.operation_id) };
|
||||
}
|
||||
if (ledgerPreparation.entry) this.ledger.execute(input.operation_id);
|
||||
} catch (error) {
|
||||
const ledgerCode = String(error?.message ?? error);
|
||||
try {
|
||||
this.task.setCommand(input.seq, input.simulation_tick ?? this.tick);
|
||||
this.tick = this.task.state.simulation_tick;
|
||||
} catch { /* protocol has already accepted the command; retain ledger error */ }
|
||||
try {
|
||||
this.timeline.appendEvent(input.seq, this.tick, "COMMAND_REJECTED", { code: ledgerCode, operation_id: input.operation_id ?? null });
|
||||
appendCoreEvent(this.core, input.seq, this.tick, "COMMAND_REJECTED", { code: ledgerCode, operation_id: input.operation_id ?? null });
|
||||
} catch { /* preserve the original ledger diagnostic */ }
|
||||
return this.error(input, ledgerCode.startsWith("ledger_") ? ledgerCode : "ledger", ledgerCode);
|
||||
}
|
||||
try {
|
||||
this.task.setCommand(input.seq, input.simulation_tick ?? this.tick);
|
||||
this.tick = this.task.state.simulation_tick;
|
||||
} catch (error) {
|
||||
return this.error(input, "task_state", String(error?.message ?? error));
|
||||
}
|
||||
const existingOperation = this.operationState(input);
|
||||
if (existingOperation?.response) {
|
||||
if (existingOperation.kind !== input.kind) return this.error(input, "recovery_operation_conflict", "operation_id_conflict");
|
||||
@@ -237,21 +490,54 @@ export class SimSession {
|
||||
operation.result_hash = fnv1a64(response.payload ?? {});
|
||||
operation.response = response;
|
||||
}
|
||||
try { this.ledger.verify(input.operation_id, response); this.ledger.commit(input.operation_id, input.seq); } catch (error) {
|
||||
const ledgerCode = String(error?.message ?? error);
|
||||
return this.error(input, ledgerCode.startsWith("ledger_") ? ledgerCode : "ledger", ledgerCode);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
const deviceResponse = (kind, payload) => {
|
||||
const result = this.devices.command(kind, payload ?? {});
|
||||
try {
|
||||
if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904);
|
||||
else if (kind === "IO_RECOVER" && result.status === "verified") this.task.recover();
|
||||
else if (kind === "TOOL_FAULT" && payload?.fault === true) this.task.fault(-905);
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
if (kind === "ESTOP") this.deviceIo.estop(payload?.active === true);
|
||||
if (kind === "IO_RECOVER" && result.status === "verified") this.deviceIo.interlockReset();
|
||||
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
|
||||
return finish(this.envelope(responseKind, input, {
|
||||
...result,
|
||||
command: kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
...result,
|
||||
command: kind,
|
||||
task: this.task.export(),
|
||||
device_io: this.deviceIo.export(),
|
||||
motion: this.motion.export(),
|
||||
operation_id: input.operation_id ?? null,
|
||||
simulation_tick: this.tick,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
};
|
||||
|
||||
const deviceIoResponse = (kind, payload = {}) => {
|
||||
let result;
|
||||
if (kind === "ENCODER_SET") result = this.deviceIo.encoderSet(payload.channel, payload);
|
||||
else if (kind === "COOLANT_COMMAND") result = this.deviceIo.coolantCommand(payload);
|
||||
else if (kind === "COOLANT_FAULT") result = this.deviceIo.coolantFault(payload.fault === true);
|
||||
else if (kind === "DOOR_SET") result = this.deviceIo.doorSet(payload.open === true);
|
||||
else if (kind === "INTERLOCK_SET") result = this.deviceIo.interlockSet(payload.input_ok === true);
|
||||
else if (kind === "INTERLOCK_RESET") result = this.deviceIo.interlockReset();
|
||||
else if (kind === "DEVICE_ESTOP") result = this.deviceIo.estop(payload.active === true);
|
||||
else if (kind === "DEVICE_ACK_ISSUE") result = this.deviceIo.issueAck(input.operation_id ?? payload.operation_id, payload.timeout_ticks ?? 10);
|
||||
else if (kind === "DEVICE_ACK_ABORT") result = this.deviceIo.abortAck(payload.operation_id);
|
||||
else if (kind === "DEVICE_ACK") result = this.deviceIo.ack(payload.operation_id);
|
||||
else result = this.deviceIo.fail("device_io_command_unsupported");
|
||||
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
|
||||
return finish(this.envelope(responseKind, input, { ...result, command: kind, device_io: this.deviceIo.export(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
};
|
||||
|
||||
switch (input.kind) {
|
||||
case "HELLO":
|
||||
return finish(this.envelope("CAPABILITIES", input, capabilities(this.contextId)), "verified");
|
||||
@@ -261,27 +547,57 @@ export class SimSession {
|
||||
if (createResult !== 0) return finish(this.coreError(input, createResult, "sim_create"), "failed");
|
||||
this.created = true;
|
||||
}
|
||||
return finish(this.envelope("SESSION_READY", input, { state_hash: fnv1a64({ tick: this.tick }), subscriptions: [] }), "verified");
|
||||
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 "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");
|
||||
if (input.payload?.configuration) {
|
||||
const preflight = preflightConfiguration(input.payload.configuration);
|
||||
if (preflight.status !== "ready") {
|
||||
return finish(this.envelope("ERROR", input, {
|
||||
status: "blocked",
|
||||
code: "configuration_preflight",
|
||||
message_key: "configuration.preflight_blocked",
|
||||
retryable: false,
|
||||
preflight,
|
||||
}), "failed");
|
||||
let startupLoaded = false;
|
||||
const configuration = input.payload?.configuration ?? null;
|
||||
if (configuration) {
|
||||
const startupWorkspace = configuration.workspace ?? configuration.files ?? configuration.workspace_files;
|
||||
if (startupWorkspace) {
|
||||
const startup = new ConfigurationStartupStateMachine({
|
||||
workspace: startupWorkspace,
|
||||
entrypoint: configuration.entrypoint,
|
||||
variables: configuration.variables ?? {},
|
||||
maxDepth: configuration.maxDepth ?? 32,
|
||||
core: this.core,
|
||||
coreCreated: this.created,
|
||||
task: this.task,
|
||||
halManifest: configuration.halManifest ?? configuration.hal_manifest ?? configuration.manifest ?? [],
|
||||
});
|
||||
const startupReport = startup.run();
|
||||
if (startupReport.status !== "ready") {
|
||||
return finish(this.envelope("ERROR", input, {
|
||||
status: "blocked",
|
||||
code: "configuration_startup",
|
||||
message_key: "configuration.startup_blocked",
|
||||
retryable: false,
|
||||
startup: startupReport,
|
||||
}), "failed");
|
||||
}
|
||||
this.configurationState = startupReport;
|
||||
this.created = true;
|
||||
startupLoaded = true;
|
||||
} else {
|
||||
const preflight = preflightConfiguration(input.payload.configuration);
|
||||
if (preflight.status !== "ready") {
|
||||
return finish(this.envelope("ERROR", input, {
|
||||
status: "blocked",
|
||||
code: "configuration_preflight",
|
||||
message_key: "configuration.preflight_blocked",
|
||||
retryable: false,
|
||||
preflight,
|
||||
}), "failed");
|
||||
}
|
||||
this.configurationState = {
|
||||
entrypoint: input.payload.configuration.entrypoint ?? null,
|
||||
status: preflight.status,
|
||||
resource_count: preflight.resource_count,
|
||||
state_hash: fnv1a64(preflight),
|
||||
};
|
||||
}
|
||||
this.configurationState = {
|
||||
entrypoint: input.payload.configuration.entrypoint ?? null,
|
||||
status: preflight.status,
|
||||
resource_count: preflight.resource_count,
|
||||
state_hash: fnv1a64(preflight),
|
||||
};
|
||||
}
|
||||
if (input.payload?.hal) {
|
||||
const preflight = preflightHalConfiguration(input.payload.hal);
|
||||
@@ -300,28 +616,268 @@ export class SimSession {
|
||||
state_hash: fnv1a64(preflight),
|
||||
};
|
||||
}
|
||||
const loadResult = call(this.core, "sim_load_workspace", rootPath);
|
||||
return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, state_hash: fnv1a64({ tick: this.tick }) })) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
|
||||
const configuredKinematics = input.payload?.kinematics ?? configuration?.kinematics?.type ?? null;
|
||||
if (configuredKinematics) {
|
||||
const kinematics = this.authoritative.setKinematics(configuredKinematics);
|
||||
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);
|
||||
return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, {
|
||||
status: "verified",
|
||||
command: input.kind,
|
||||
state_hash: this.task.hash(),
|
||||
task: this.task.export(),
|
||||
...(this.configurationState?.schema === "configuration-startup-runtime.v1" ? { configuration: this.configurationState } : {}),
|
||||
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
|
||||
}
|
||||
case "SIM_STEP": {
|
||||
const cycles = input.payload?.cycles;
|
||||
const limit = validateLimit("max_tick_step", cycles);
|
||||
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
|
||||
const stepResult = call(this.core, "sim_step", cycles);
|
||||
if (stepResult !== 0) return finish(this.coreError(input, stepResult, "sim_step"), "failed");
|
||||
this.tick += cycles;
|
||||
this.python.setTick(this.tick);
|
||||
const protocolTick = call(this.core, "sim_protocol_set_tick", this.tick);
|
||||
if (protocolTick !== 0) return finish(this.error(input, "protocol_tick", "tick_update_failed"), "failed");
|
||||
const snapshot = this.timeline.checkpoint(input.seq, this.tick, { tick: this.tick, cycles }, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, state_hash: snapshot.state_hash, snapshot }));
|
||||
let program_steps = [];
|
||||
try {
|
||||
this.scheduler.step(cycles, {
|
||||
task: () => {
|
||||
program_steps.push(...this.program.tick(1));
|
||||
},
|
||||
tp: (nextTick) => {
|
||||
if (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);
|
||||
},
|
||||
kinematics: () => {
|
||||
if (!["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);
|
||||
if (stepResult !== 0) throw new Error(`core_sim_step_${stepResult}`);
|
||||
this.tick = nextTick;
|
||||
this.task.setTick(nextTick);
|
||||
const motionState = this.motion.step(nextTick);
|
||||
if (motionState.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
if (this.program.loaded) this.authoritative.applyMotion(this.motion);
|
||||
const halState = this.hal.step(nextTick);
|
||||
if (halState.status === "error") throw new Error(halState.code);
|
||||
const deviceIoState = this.deviceIo.step(nextTick);
|
||||
if (deviceIoState.status === "blocked") throw new Error(deviceIoState.code);
|
||||
const plcState = this.plc.scan(nextTick);
|
||||
if (plcState.status === "error") throw new Error(plcState.code);
|
||||
const ackState = this.ioAck.step(nextTick);
|
||||
if (ackState.status === "error") throw new Error(ackState.code);
|
||||
const callbackClock = this.callbacks.setTick(nextTick);
|
||||
if (callbackClock.status === "blocked") throw new Error(callbackClock.code);
|
||||
this.python.setTick(nextTick);
|
||||
const protocolTick = call(this.core, "sim_protocol_set_tick", nextTick);
|
||||
if (protocolTick !== 0) throw new Error(`protocol_tick_${protocolTick}`);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "scheduler", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
const program = this.program.loaded ? this.program.snapshot() : null;
|
||||
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");
|
||||
}
|
||||
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() };
|
||||
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 }));
|
||||
}
|
||||
case "PROGRAM_LOAD": {
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed");
|
||||
const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source;
|
||||
const result = this.program.load(source, input.payload?.path ?? null);
|
||||
if (result.status === "verified") {
|
||||
this.authoritative.reset();
|
||||
this.task.setProgram("loaded", 0);
|
||||
}
|
||||
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");
|
||||
}
|
||||
case "PROGRAM_RUN": {
|
||||
const result = this.program.start();
|
||||
if (result.status === "verified") {
|
||||
this.task.setMode("AUTO");
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
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(),
|
||||
command: input.kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": {
|
||||
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); }
|
||||
}
|
||||
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");
|
||||
}
|
||||
case "PROGRAM_RESTART": {
|
||||
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 (this.task.state.state === "STOPPED") this.task.recover();
|
||||
this.task.setMode("AUTO");
|
||||
this.authoritative.reset();
|
||||
this.task.setProgram("loaded", 0);
|
||||
}
|
||||
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");
|
||||
}
|
||||
case "PROGRAM_STEP": {
|
||||
if (this.task.state.state === "READY") {
|
||||
this.task.setMode("AUTO");
|
||||
this.task.transition("RUNNING");
|
||||
}
|
||||
const result = this.program.step();
|
||||
if (result.status === "verified") {
|
||||
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");
|
||||
} 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(),
|
||||
command: input.kind,
|
||||
operation_id: input.operation_id ?? null,
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "PROGRAM_SEEK": {
|
||||
const result = this.program.seek(input.payload?.line);
|
||||
if (result.status === "verified") this.task.setProgram("paused", result.line);
|
||||
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");
|
||||
}
|
||||
case "MDI_EXECUTE": {
|
||||
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "mdi_while_program_active"), "failed");
|
||||
if (this.task.state.state === "STOPPED") this.task.recover();
|
||||
const result = this.program.executeBlock(input.payload?.block ?? input.payload?.command);
|
||||
if (result.status === "verified") { this.task.setMode("MDI"); this.task.setProgram("loaded", this.program.currentLine); }
|
||||
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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
case "JOG_START": case "JOG_STOP": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
|
||||
let result;
|
||||
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");
|
||||
}
|
||||
case "SPINDLE_COMMAND": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":
|
||||
case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE":
|
||||
case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT":
|
||||
case "PROBE_CONFIG": case "LIMIT_SET": case "ESTOP": case "IO_RECOVER":
|
||||
case "PROBE_CONFIG": case "IO_RECOVER":
|
||||
return deviceResponse(input.kind, input.payload);
|
||||
case "LIMIT_SET": {
|
||||
const result = this.motion.setLimit(input.payload?.axis, input.payload?.active === true);
|
||||
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");
|
||||
}
|
||||
case "PLC_CONFIG": case "PLC_SET": case "PLC_TRIGGER": {
|
||||
const result = input.kind === "PLC_CONFIG" ? this.plc.configure(input.payload) : input.kind === "PLC_SET" ? this.plc.setRegister(input.payload?.register, input.payload?.value) : this.plc.trigger();
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), plc: this.plc.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "CONFIGURATION_BATCH": {
|
||||
const result = this.configurationBatch.load(input.payload?.entries, { context_id: this.contextId });
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, configuration_batch: this.configurationBatch.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "HAL_COMMAND": case "HAL_SET": {
|
||||
const payload = input.payload ?? {};
|
||||
let result;
|
||||
if (input.kind === "HAL_SET") {
|
||||
result = payload.target === "param" ? this.hal.setParam(payload.name, payload.value, payload.type) : this.hal.setPin(payload.name, payload.value, payload.type);
|
||||
} else if (payload.action === "configure") {
|
||||
result = this.hal.configure(payload);
|
||||
} else if (payload.action === "thread") {
|
||||
result = this.hal.registerThread(payload.name, payload.period_ticks);
|
||||
} else if (payload.action === "component") {
|
||||
result = this.hal.registerComponent(payload.instance ?? payload.name, payload.type, payload.thread ?? "servo-thread");
|
||||
} else if (payload.action === "connect") {
|
||||
result = this.hal.connect(payload.source, payload.target);
|
||||
} else if (payload.action === "failure") {
|
||||
result = this.hal.setFailure(payload.instance, payload.failed === true);
|
||||
} else if (payload.action === "step") {
|
||||
result = this.hal.step(payload.tick ?? this.tick);
|
||||
} else {
|
||||
result = this.hal.fail("hal_command_unsupported");
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), hal_runtime: this.hal.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "ENCODER_SET": case "COOLANT_COMMAND": case "COOLANT_FAULT": case "DOOR_SET": case "INTERLOCK_SET": case "INTERLOCK_RESET": case "DEVICE_ESTOP": case "DEVICE_ACK_ISSUE": case "DEVICE_ACK": case "DEVICE_ACK_ABORT":
|
||||
return deviceIoResponse(input.kind, input.payload);
|
||||
case "IO_COMMAND": case "IO_ACK": case "IO_TIMEOUT": {
|
||||
let result;
|
||||
if (input.kind === "IO_COMMAND") {
|
||||
result = this.ioAck.issue(input.operation_id, input.payload?.kind ?? "IO", this.tick, input.payload?.timeout_ticks ?? 10);
|
||||
if (result.status === "verified") this.deviceIo.issueAck(input.operation_id, input.payload?.timeout_ticks ?? 10);
|
||||
} else if (input.kind === "IO_ACK") {
|
||||
result = this.ioAck.ack(input.payload?.operation_id, this.tick);
|
||||
this.deviceIo.ack(input.payload?.operation_id);
|
||||
} else {
|
||||
const targetTick = this.tick + (input.payload?.ticks ?? 0);
|
||||
result = this.ioAck.step(targetTick);
|
||||
this.deviceIo.ackStep(targetTick);
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, io_ack: this.ioAck.export(), device_io: this.deviceIo.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "REMAP_REGISTER": case "REMAP_CALL": case "VCP_REGISTER": case "VCP_UPDATE": case "FILTER_EXEC": {
|
||||
const result = input.kind === "REMAP_REGISTER"
|
||||
? this.callbacks.registerRemap(input.payload?.name, input.payload)
|
||||
: input.kind === "REMAP_CALL"
|
||||
? this.callbacks.invoke("remap", input.payload?.name, input.payload?.phase ?? "prolog", input.payload?.args ?? [])
|
||||
: input.kind === "VCP_REGISTER"
|
||||
? this.callbacks.registerVcp(input.payload?.name, input.payload?.pins ?? [])
|
||||
: input.kind === "VCP_UPDATE"
|
||||
? this.callbacks.updateVcp(input.payload?.name, input.payload?.pin, input.payload?.value)
|
||||
: this.callbacks.filter(input.payload?.name, input.payload?.source ?? "", input.payload?.args ?? []);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, callbacks: this.callbacks.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "ESTOP": {
|
||||
const motionResult = this.motion.estop(input.payload?.active === true);
|
||||
const deviceResult = deviceResponse(input.kind, input.payload);
|
||||
if (motionResult.status === "error") return finish(this.envelope("ERROR", input, { ...motionResult, task: this.task.export() }), "failed");
|
||||
return deviceResult;
|
||||
}
|
||||
case "PYTHON_IMPORT": {
|
||||
const result = this.python.importModule(input.payload?.module);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
|
||||
@@ -335,7 +891,7 @@ export class SimSession {
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "SNAPSHOT_REQUEST":
|
||||
return finish(this.envelope("STATE_SNAPSHOT", input, { snapshot_schema: 1, tick: this.tick, state_hash: fnv1a64({ tick: this.tick }), snapshot: this.timeline.snapshots.latest() }));
|
||||
return finish(this.envelope("STATE_SNAPSHOT", input, { snapshot_schema: 1, tick: this.tick, task: this.task.export(), state_hash: this.task.hash(), snapshot: this.timeline.snapshots.latest() }));
|
||||
case "CHECKPOINT": {
|
||||
try {
|
||||
const operation = text(input.operation_id) ? this.operations.get(input.operation_id) : null;
|
||||
@@ -354,9 +910,19 @@ export class SimSession {
|
||||
const target = input.payload?.operation_id;
|
||||
const operation = text(target) ? this.operations.get(target) : null;
|
||||
if (operation && operation.status !== "verified") operation.status = "aborted";
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: operation ? "aborted" : "verified", operation_id: target ?? null }));
|
||||
let ledgerResult = null;
|
||||
if (text(target)) {
|
||||
try { ledgerResult = this.ledger.abort(target); } catch (error) { ledgerResult = { status: "missing", error: String(error?.message ?? error) }; }
|
||||
}
|
||||
if (this.task.state.state !== "DESTROYED") {
|
||||
try { this.task.transition("STOPPED"); } catch { /* target abort can be bookkeeping-only */ }
|
||||
}
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: ledgerResult?.status === "too_late" ? "too_late" : operation ? "aborted" : "verified", operation_id: target ?? null, ledger: ledgerResult }));
|
||||
}
|
||||
case "DESTROY":
|
||||
this.program.destroy();
|
||||
this.task.destroy();
|
||||
this.scheduler.destroy();
|
||||
call(this.core, "sim_destroy");
|
||||
this.created = false;
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", destroyed: true, replay: this.timeline.export() }));
|
||||
|
||||
Reference in New Issue
Block a user