194 lines
6.5 KiB
TypeScript
194 lines
6.5 KiB
TypeScript
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<VirtualControllerStateMachine["snapshot"]>;
|
|
runtime: IrExecutionRuntimeSnapshot;
|
|
motion?: ReturnType<MotionQueue["snapshot"]>;
|
|
io: ReturnType<IoImageRuntime["snapshot"]>;
|
|
bridge?: ReturnType<KdlRuntimeBridge["snapshot"]>;
|
|
}
|
|
|
|
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<ExecutableInstruction, { kind: "MOVEJ" | "MOVEL" | "MOVEC" | "RUN_PATH" | "RUN_OPERATION" }> {
|
|
return (
|
|
instruction.kind === "MOVEJ" ||
|
|
instruction.kind === "MOVEL" ||
|
|
instruction.kind === "MOVEC" ||
|
|
instruction.kind === "RUN_PATH" ||
|
|
instruction.kind === "RUN_OPERATION"
|
|
);
|
|
}
|