import type { ExecutableInstruction, SemanticProgramIr } from "../grl/ir/index.js"; import { IoImageRuntime, type IoRuntimeOptions } from "../runtime/ioRuntime.js"; import { KdlRuntimeBridge } from "../runtime/kdlBridge.js"; import { MotionQueue, type MotionPlanner } from "../runtime/motionQueue.js"; import type { RobotHandle } from "../kdl/types.js"; import { IrExecutionRuntime, type IrExecutionRuntimeSnapshot, type RuntimeStepResult } from "./runtime.js"; import { VirtualControllerStateMachine } from "./stateMachine.js"; import type { RuntimeTraceEvent } from "./trace.js"; export interface VirtualControllerLoadOptions { entryProcedure?: string; startJoints: number[]; sampleTime: number; planner?: MotionPlanner; robotHandle?: RobotHandle; io?: IoRuntimeOptions; } export interface VirtualControllerSnapshot { state: ReturnType; runtime: IrExecutionRuntimeSnapshot; motion?: ReturnType; io: ReturnType; bridge?: ReturnType; } export class VirtualController { readonly stateMachine = new VirtualControllerStateMachine(); private ioValue = new IoImageRuntime(); private runtimeValue: IrExecutionRuntime | undefined; private motionQueueValue: MotionQueue | undefined; private bridgeValue: KdlRuntimeBridge | undefined; private loadedOptions: VirtualControllerLoadOptions | undefined; get io(): IoImageRuntime { return this.ioValue; } get runtime(): IrExecutionRuntime | undefined { return this.runtimeValue; } get motionQueue(): MotionQueue | undefined { return this.motionQueueValue; } get bridge(): KdlRuntimeBridge | undefined { return this.bridgeValue; } load(program: SemanticProgramIr, options: VirtualControllerLoadOptions): RuntimeTraceEvent | undefined { this.loadedOptions = options; this.ioValue = new IoImageRuntime(options.io); this.motionQueueValue = new MotionQueue({ startJoints: options.startJoints, sampleTime: options.sampleTime, ...(options.planner ? { planner: options.planner } : {}), ...(options.robotHandle !== undefined ? { robotHandle: options.robotHandle } : {}) }); this.runtimeValue = new IrExecutionRuntime({ ...(options.entryProcedure ? { entryProcedure: options.entryProcedure } : {}), hooks: { onMotion: (instruction, runtime) => { this.bridgeValue?.enqueueInstruction(instruction); runtime.trace("motion", { instruction, data: { queued: true } }); }, onIo: (instruction, runtime) => { const result = this.ioValue.executeInstruction(instruction); runtime.trace("io", { instruction, data: { result } }); }, onWait: (instruction, runtime) => { const waitKey = `$wait:${runtime.currentFrame?.procedure ?? "main"}:${runtime.currentFrame?.pc ?? 0}`; if (runtime.getVariable(waitKey) === undefined) { runtime.setVariable(waitKey, runtime.virtualTime, "global"); } const elapsed = runtime.virtualTime - Number(runtime.getVariable(waitKey) ?? runtime.virtualTime); const result = this.ioValue.evaluateWait(instruction, elapsed); runtime.trace("wait", { instruction, data: { status: result.status, elapsed } }); if (result.status === "satisfied" || result.status === "timeout") { if (result.onTimeout?.kind === "alarm") { runtime.pushAlarm(result.onTimeout.value, result.onTimeout.value, "warning", runtime.locateInstruction(instruction)); } return undefined; } if (result.status === "hold-stop") { this.stateMachine.dispatch("hold"); } return { status: "blocked", instruction }; } } }); this.runtimeValue.load(program, options.entryProcedure); this.bridgeValue = new KdlRuntimeBridge(program, this.motionQueueValue); this.stateMachine.dispatch("load"); return this.runtimeValue.snapshot().trace.at(-1); } run(maxSteps?: number): RuntimeStepResult { const transition = this.stateMachine.dispatch("run"); if (!transition.ok) { return blockedTransitionResult(transition.diagnostic); } const result = this.requireRuntime().run(maxSteps); if (result.status === "completed") { this.stateMachine.dispatch("stop"); } else if (result.status === "breakpoint") { this.stateMachine.dispatch("pause"); } return result; } pause(): void { this.stateMachine.dispatch("pause"); } stop(): void { this.stateMachine.dispatch("stop"); } reset(): void { const options = this.loadedOptions; this.stateMachine.dispatch("reset"); if (!options || !this.runtimeValue || !this.runtimeValue.program) { return; } this.runtimeValue.reset(options.entryProcedure); this.motionQueueValue?.clear(); } step(): RuntimeStepResult { const transition = this.stateMachine.dispatch("step"); if (!transition.ok) { return blockedTransitionResult(transition.diagnostic); } return this.requireRuntime().step(); } advance(deltaTime: number): void { this.io.advance(deltaTime); this.motionQueueValue?.advance(deltaTime); if (this.runtimeValue) { this.runtimeValue.virtualTime += deltaTime; } } snapshot(): VirtualControllerSnapshot { return { state: this.stateMachine.snapshot(), runtime: this.requireRuntime().snapshot(), ...(this.motionQueueValue ? { motion: this.motionQueueValue.snapshot() } : {}), io: this.ioValue.snapshot(), ...(this.bridgeValue ? { bridge: this.bridgeValue.snapshot() } : {}) }; } private requireRuntime(): IrExecutionRuntime { if (!this.runtimeValue) { throw new Error("Controller has not loaded a program"); } return this.runtimeValue; } } function blockedTransitionResult(diagnostic: RuntimeStepResult["diagnostic"]): RuntimeStepResult { return { status: "blocked", ...(diagnostic ? { diagnostic } : {}) }; } export function isMotionInstruction( instruction: ExecutableInstruction ): instruction is Extract { return ( instruction.kind === "MOVEJ" || instruction.kind === "MOVEL" || instruction.kind === "MOVEC" || instruction.kind === "RUN_PATH" || instruction.kind === "RUN_OPERATION" ); }