同步KDL工程源码到云仓库
This commit is contained in:
205
kdl-wasm/web/tests/grl/ioCompile.test.ts
Normal file
205
kdl-wasm/web/tests/grl/ioCompile.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type {
|
||||
GrlDataDeclaration,
|
||||
GrlOperationDeclaration,
|
||||
GrlPathDeclaration,
|
||||
GrlProcedureDeclaration,
|
||||
GrlTargetDeclaration
|
||||
} from "../../src/grl/ast/index.js";
|
||||
import type { PathEventInstruction } from "../../src/grl/ir/index.js";
|
||||
import { parseGrl } from "../../src/grl/parser/index.js";
|
||||
import {
|
||||
buildMotionContext,
|
||||
compileOperation,
|
||||
compileOperationActionIo,
|
||||
compilePathEventIo,
|
||||
compilePathToPlanRequest,
|
||||
parseIoFlowStatements,
|
||||
type IoMap
|
||||
} from "../../src/grl/semantic/index.js";
|
||||
|
||||
const PROGRAM = `language grl 0.1
|
||||
module Main
|
||||
const speed v = joint(50 %)
|
||||
const zone zf = fine
|
||||
target home = joint_target { joints: [0 deg] }
|
||||
path io_path {
|
||||
defaults { speed: v, zone: zf }
|
||||
point p0 movej home
|
||||
event at p0 distance 0 mm pulse io.do[20] duration 100 ms
|
||||
}
|
||||
operation io_op {
|
||||
kind: handling
|
||||
path: io_path
|
||||
start_action:
|
||||
wait io.di[4] == true timeout 500 ms on_timeout alarm "part missing"
|
||||
end_action:
|
||||
pulse io.do[5] duration 250 ms
|
||||
}
|
||||
proc main()
|
||||
io.do[1] = true
|
||||
io.go[2] = 16
|
||||
io.alias.grip_close = false
|
||||
wait all(io.di[1] == true, io.di[2] == false) timeout 2 s on_timeout alarm "Clamp close timeout"
|
||||
wait any(rising(io.di[3]), falling(io.di[4]), changed(io.ai[1]))
|
||||
wait io.di[5] == true timeout 1 s on_timeout call recover
|
||||
pulse io.do[3] duration 200 ms
|
||||
end
|
||||
end
|
||||
`;
|
||||
|
||||
const IO_MAP: IoMap = {
|
||||
aliases: {
|
||||
grip_close: { domain: "do", index: 6, raw: "io.do[6]" }
|
||||
},
|
||||
allowedRanges: {
|
||||
ai: { min: 1, max: 8 },
|
||||
di: { min: 1, max: 16 },
|
||||
do: { min: 1, max: 32 },
|
||||
go: { min: 1, max: 4 }
|
||||
}
|
||||
};
|
||||
|
||||
function declarations() {
|
||||
return parseGrl(PROGRAM).module.declarations;
|
||||
}
|
||||
|
||||
function motionContext(decls = declarations()) {
|
||||
return buildMotionContext(
|
||||
decls.filter(
|
||||
(decl): decl is GrlDataDeclaration | GrlTargetDeclaration =>
|
||||
decl.kind === "DataDeclaration" || decl.kind === "TargetDeclaration"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function pathsByName(paths: GrlPathDeclaration[]) {
|
||||
return new Map(paths.map((path) => [path.name, path]));
|
||||
}
|
||||
|
||||
describe("GRL IO, wait, and pulse compilation", () => {
|
||||
it("compiles procedure IO writes, wait conditions, timeout actions, and pulse traces", () => {
|
||||
const procedure = declarations().find(
|
||||
(decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration"
|
||||
)!;
|
||||
|
||||
const instructions = parseIoFlowStatements(procedure.bodyTokens, IO_MAP);
|
||||
|
||||
expect(instructions).toHaveLength(7);
|
||||
expect(instructions[0]).toMatchObject({
|
||||
kind: "IO_WRITE",
|
||||
target: { domain: "do", index: 1, raw: "io.do[1]" },
|
||||
value: true
|
||||
});
|
||||
expect(instructions[1]).toMatchObject({
|
||||
kind: "IO_WRITE",
|
||||
target: { domain: "go", index: 2, raw: "io.go[2]" },
|
||||
value: 16
|
||||
});
|
||||
expect(instructions[2]).toMatchObject({
|
||||
kind: "IO_WRITE",
|
||||
target: { domain: "do", index: 6, raw: "io.do[6]" },
|
||||
value: false
|
||||
});
|
||||
expect(instructions[3]).toMatchObject({
|
||||
kind: "WAIT",
|
||||
condition: "all ( io . di [ 1 ] == true , io . di [ 2 ] == false )",
|
||||
timeout: 2,
|
||||
onTimeout: { kind: "alarm", value: "Clamp close timeout" }
|
||||
});
|
||||
expect(instructions[4]).toMatchObject({
|
||||
kind: "WAIT",
|
||||
condition: "any ( rising ( io . di [ 3 ] ) , falling ( io . di [ 4 ] ) , changed ( io . ai [ 1 ] ) )"
|
||||
});
|
||||
expect(instructions[5]).toMatchObject({
|
||||
kind: "WAIT",
|
||||
condition: "io . di [ 5 ] == true",
|
||||
timeout: 1,
|
||||
onTimeout: { kind: "call", value: "recover" }
|
||||
});
|
||||
expect(instructions[6]).toMatchObject({
|
||||
kind: "PULSE",
|
||||
target: { domain: "do", index: 3, raw: "io.do[3]" },
|
||||
duration: 0.2,
|
||||
trace: [
|
||||
{ time: 0, action: "set", target: { domain: "do", index: 3 }, value: true },
|
||||
{ time: 0.2, action: "reset", target: { domain: "do", index: 3 }, value: false }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it("validates IO addresses against configured ranges", () => {
|
||||
const procedure = parseGrl(`language grl 0.1
|
||||
module Main
|
||||
proc main()
|
||||
io.do[99] = true
|
||||
end
|
||||
end
|
||||
`).module.declarations.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;
|
||||
|
||||
expect(() =>
|
||||
parseIoFlowStatements(procedure.bodyTokens, { allowedRanges: { do: { min: 1, max: 16 } } })
|
||||
).toThrowError(expect.objectContaining({ code: "GRL_IO_ADDRESS_NOT_FOUND" }));
|
||||
});
|
||||
|
||||
it("expands path event IO metadata into pulse IR without entering KDL motion segments", () => {
|
||||
const decls = declarations();
|
||||
const path = decls.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!;
|
||||
const compiled = compilePathToPlanRequest(path, motionContext(decls), {
|
||||
startJoints: [0],
|
||||
sampleTime: 0.004
|
||||
});
|
||||
|
||||
expect(compiled.request.segments).toHaveLength(1);
|
||||
expect(compiled.request.events).toHaveLength(1);
|
||||
expect(compilePathEventIo(compiled.events[0]!, IO_MAP)).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "PULSE",
|
||||
target: { domain: "do", index: 20, raw: "io.do[20]" },
|
||||
duration: 0.1
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it("expands operation action metadata into wait and pulse IR with preserved units", () => {
|
||||
const decls = declarations();
|
||||
const operation = decls.find(
|
||||
(decl): decl is GrlOperationDeclaration => decl.kind === "OperationDeclaration"
|
||||
)!;
|
||||
const paths = decls.filter((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration");
|
||||
const compiled = compileOperation(operation, pathsByName(paths));
|
||||
|
||||
expect(compileOperationActionIo(compiled.startActions[0]!, IO_MAP)).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "WAIT",
|
||||
condition: "io . di [ 4 ] == true",
|
||||
timeout: 0.5,
|
||||
onTimeout: { kind: "alarm", value: "part missing" }
|
||||
})
|
||||
]);
|
||||
expect(compileOperationActionIo(compiled.endActions[0]!, IO_MAP)).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "PULSE",
|
||||
target: { domain: "do", index: 5, raw: "io.do[5]" },
|
||||
duration: 0.25
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it("lexes statement fallback metadata so unit literals and booleans remain typed", () => {
|
||||
const event: PathEventInstruction = {
|
||||
timing: "at",
|
||||
pointId: "p0",
|
||||
kind: "pulse",
|
||||
data: { statement: "pulse io.do[7] duration 125 ms" }
|
||||
};
|
||||
|
||||
expect(compilePathEventIo(event, IO_MAP)).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "PULSE",
|
||||
target: { domain: "do", index: 7, raw: "io.do[7]" },
|
||||
duration: 0.125
|
||||
})
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user