@holoscript/robotics-plugin 1.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/examples/isaac-lab-sim-to-real-bridge.holo +63 -0
- package/examples/isaac-lab-sim-to-real.holo +127 -0
- package/package.json +7 -42
- package/python/isaac_lab_bridge.py +157 -0
- package/src/__tests__/isaac-lab-interop.test.ts +210 -0
- package/src/ast.ts +80 -0
- package/src/index.ts +563 -0
- package/src/lexer.ts +307 -0
- package/src/parser.ts +473 -0
- package/src/traits/ROS2HardwareLoopTrait.ts +84 -0
- package/src/traits/types.ts +4 -0
- package/src/usd-codegen.ts +651 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +11 -0
- package/README.md +0 -3
- package/dist/ast.d.ts +0 -21
- package/dist/ast.js +0 -5
- package/dist/index.d.ts +0 -58
- package/dist/index.js +0 -44
- package/dist/lexer.d.ts +0 -52
- package/dist/lexer.js +0 -275
- package/dist/parser.d.ts +0 -22
- package/dist/parser.js +0 -202
- package/dist/usd-codegen.d.ts +0 -18
- package/dist/usd-codegen.js +0 -361
- package/examples/robot-arm-complete.holo +0 -199
- package/python/ros2_bridge.py +0 -77
package/src/parser.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HoloScript Parser
|
|
3
|
+
*
|
|
4
|
+
* Converts token stream into Abstract Syntax Tree (AST).
|
|
5
|
+
* Uses recursive descent parsing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Token, TokenType } from './lexer';
|
|
9
|
+
import {
|
|
10
|
+
CompositionNode,
|
|
11
|
+
ObjectNode,
|
|
12
|
+
PropertyValue,
|
|
13
|
+
DomainRandomizationConfig,
|
|
14
|
+
ActuatorGroupConfig,
|
|
15
|
+
} from './ast';
|
|
16
|
+
|
|
17
|
+
export class Parser {
|
|
18
|
+
private tokens: Token[];
|
|
19
|
+
private position: number = 0;
|
|
20
|
+
|
|
21
|
+
constructor(tokens: Token[]) {
|
|
22
|
+
this.tokens = tokens;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
parse(): CompositionNode {
|
|
26
|
+
return this.parseComposition();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private currentToken(): Token {
|
|
30
|
+
const token = this.tokens[this.position];
|
|
31
|
+
if (!token) {
|
|
32
|
+
throw new Error('Unexpected end of input');
|
|
33
|
+
}
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private peek(offset: number = 1): Token {
|
|
38
|
+
const pos = this.position + offset;
|
|
39
|
+
const token = pos < this.tokens.length ? this.tokens[pos] : this.tokens[this.tokens.length - 1];
|
|
40
|
+
if (!token) {
|
|
41
|
+
throw new Error('Unexpected end of input');
|
|
42
|
+
}
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private advance(): Token {
|
|
47
|
+
const token = this.currentToken();
|
|
48
|
+
this.position++;
|
|
49
|
+
return token;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private expect(type: TokenType, value?: string): Token {
|
|
53
|
+
const token = this.currentToken();
|
|
54
|
+
|
|
55
|
+
if (token.type !== type) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Expected ${type}${value ? ` "${value}"` : ''} but got ${token.type} "${token.value}" at line ${token.line}, column ${token.column}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (value !== undefined && token.value !== value) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Expected "${value}" but got "${token.value}" at line ${token.line}, column ${token.column}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return this.advance();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private parseComposition(): CompositionNode {
|
|
71
|
+
const startToken = this.expect(TokenType.KEYWORD, 'composition');
|
|
72
|
+
const name = this.expect(TokenType.STRING).value;
|
|
73
|
+
this.expect(TokenType.LBRACE);
|
|
74
|
+
|
|
75
|
+
const objects: ObjectNode[] = [];
|
|
76
|
+
let domainRandomization: DomainRandomizationConfig | undefined;
|
|
77
|
+
|
|
78
|
+
while (
|
|
79
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
80
|
+
this.currentToken().type !== TokenType.EOF
|
|
81
|
+
) {
|
|
82
|
+
// Check for domain_randomization block at composition level
|
|
83
|
+
if (
|
|
84
|
+
this.currentToken().type === TokenType.IDENTIFIER &&
|
|
85
|
+
this.currentToken().value === 'domain_randomization'
|
|
86
|
+
) {
|
|
87
|
+
this.advance();
|
|
88
|
+
this.expect(TokenType.COLON);
|
|
89
|
+
domainRandomization = this.parseDomainRandomizationBlock();
|
|
90
|
+
} else {
|
|
91
|
+
objects.push(this.parseObject());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.expect(TokenType.RBRACE);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
type: 'composition',
|
|
99
|
+
name,
|
|
100
|
+
objects,
|
|
101
|
+
domainRandomization,
|
|
102
|
+
line: startToken.line,
|
|
103
|
+
column: startToken.column,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private parseObject(): ObjectNode {
|
|
108
|
+
const startToken = this.expect(TokenType.KEYWORD, 'object');
|
|
109
|
+
const name = this.expect(TokenType.STRING).value;
|
|
110
|
+
|
|
111
|
+
// Parse traits (@trait1 @trait2 ...)
|
|
112
|
+
const traits: string[] = [];
|
|
113
|
+
while (this.currentToken().type === TokenType.TRAIT) {
|
|
114
|
+
traits.push(this.advance().value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.expect(TokenType.LBRACE);
|
|
118
|
+
|
|
119
|
+
// Parse properties (key: value)
|
|
120
|
+
const properties: Record<string, PropertyValue> = {};
|
|
121
|
+
let domainRandomization: DomainRandomizationConfig | undefined;
|
|
122
|
+
let actuatorGroups: ActuatorGroupConfig[] | undefined;
|
|
123
|
+
|
|
124
|
+
while (
|
|
125
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
126
|
+
this.currentToken().type !== TokenType.EOF
|
|
127
|
+
) {
|
|
128
|
+
// Support both object "link" @trait { ... } and object "link" { @trait ... }.
|
|
129
|
+
if (this.currentToken().type === TokenType.TRAIT) {
|
|
130
|
+
const trait = this.advance().value;
|
|
131
|
+
if (!traits.includes(trait)) {
|
|
132
|
+
traits.push(trait);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Check for domain_randomization block
|
|
136
|
+
else if (
|
|
137
|
+
this.currentToken().type === TokenType.IDENTIFIER &&
|
|
138
|
+
this.currentToken().value === 'domain_randomization'
|
|
139
|
+
) {
|
|
140
|
+
this.advance();
|
|
141
|
+
this.expect(TokenType.COLON);
|
|
142
|
+
domainRandomization = this.parseDomainRandomizationBlock();
|
|
143
|
+
}
|
|
144
|
+
// Check for actuator_group block
|
|
145
|
+
else if (
|
|
146
|
+
this.currentToken().type === TokenType.IDENTIFIER &&
|
|
147
|
+
this.currentToken().value === 'actuator_group'
|
|
148
|
+
) {
|
|
149
|
+
this.advance();
|
|
150
|
+
this.expect(TokenType.COLON);
|
|
151
|
+
const group = this.parseActuatorGroupBlock();
|
|
152
|
+
if (!actuatorGroups) actuatorGroups = [];
|
|
153
|
+
actuatorGroups.push(group);
|
|
154
|
+
}
|
|
155
|
+
// Regular property
|
|
156
|
+
else {
|
|
157
|
+
const keyName = this.expect(TokenType.IDENTIFIER).value;
|
|
158
|
+
this.expect(TokenType.COLON);
|
|
159
|
+
const value = this.parseValue();
|
|
160
|
+
properties[keyName] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.expect(TokenType.RBRACE);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
type: 'object',
|
|
168
|
+
name,
|
|
169
|
+
traits,
|
|
170
|
+
properties,
|
|
171
|
+
domainRandomization,
|
|
172
|
+
actuatorGroups,
|
|
173
|
+
line: startToken.line,
|
|
174
|
+
column: startToken.column,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private parseValue(): PropertyValue {
|
|
179
|
+
const token = this.currentToken();
|
|
180
|
+
|
|
181
|
+
// String value
|
|
182
|
+
if (token.type === TokenType.STRING) {
|
|
183
|
+
return this.advance().value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Number value
|
|
187
|
+
if (token.type === TokenType.NUMBER) {
|
|
188
|
+
return parseFloat(this.advance().value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Array value [1, 2, 3]
|
|
192
|
+
if (token.type === TokenType.LBRACKET) {
|
|
193
|
+
return this.parseArray();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Function call (for future: diagonal([1, 2, 3]))
|
|
197
|
+
if (token.type === TokenType.IDENTIFIER && this.peek().type === TokenType.LBRACKET) {
|
|
198
|
+
this.advance(); // Skip function name for now
|
|
199
|
+
const args = this.parseArray();
|
|
200
|
+
|
|
201
|
+
// For now, treat function calls as arrays (e.g., diagonal([1,2,3]) -> [1,2,3])
|
|
202
|
+
// In future, could preserve function name metadata
|
|
203
|
+
return args;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Unexpected value type ${token.type} "${token.value}" at line ${token.line}, column ${token.column}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private parseArray(): PropertyValue[] {
|
|
212
|
+
this.expect(TokenType.LBRACKET);
|
|
213
|
+
|
|
214
|
+
const values: PropertyValue[] = [];
|
|
215
|
+
|
|
216
|
+
while (
|
|
217
|
+
this.currentToken().type !== TokenType.RBRACKET &&
|
|
218
|
+
this.currentToken().type !== TokenType.EOF
|
|
219
|
+
) {
|
|
220
|
+
if (this.currentToken().type === TokenType.NUMBER) {
|
|
221
|
+
values.push(parseFloat(this.advance().value));
|
|
222
|
+
} else if (this.currentToken().type === TokenType.STRING) {
|
|
223
|
+
values.push(this.advance().value);
|
|
224
|
+
} else if (this.currentToken().type === TokenType.LBRACKET) {
|
|
225
|
+
// Nested array
|
|
226
|
+
values.push(this.parseArray());
|
|
227
|
+
} else {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Unexpected token in array: ${this.currentToken().type} at line ${this.currentToken().line}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Optional comma
|
|
234
|
+
if (this.currentToken().type === TokenType.COMMA) {
|
|
235
|
+
this.advance();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.expect(TokenType.RBRACKET);
|
|
240
|
+
return values;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private parseDomainRandomizationBlock(): DomainRandomizationConfig {
|
|
244
|
+
this.expect(TokenType.LBRACE);
|
|
245
|
+
const config: DomainRandomizationConfig = {};
|
|
246
|
+
|
|
247
|
+
while (
|
|
248
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
249
|
+
this.currentToken().type !== TokenType.EOF
|
|
250
|
+
) {
|
|
251
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
252
|
+
this.expect(TokenType.COLON);
|
|
253
|
+
|
|
254
|
+
switch (key) {
|
|
255
|
+
case 'physics':
|
|
256
|
+
config.physics = this.parsePhysicsRandomization();
|
|
257
|
+
break;
|
|
258
|
+
case 'actuator':
|
|
259
|
+
config.actuator = this.parseActuatorRandomization();
|
|
260
|
+
break;
|
|
261
|
+
case 'observation':
|
|
262
|
+
config.observation = this.parseObservationRandomization();
|
|
263
|
+
break;
|
|
264
|
+
case 'initialState':
|
|
265
|
+
config.initialState = this.parseInitialStateRandomization();
|
|
266
|
+
break;
|
|
267
|
+
case 'disturbance':
|
|
268
|
+
config.disturbance = this.parseDisturbanceRandomization();
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
// Skip unknown keys
|
|
272
|
+
this.parseValue();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.expect(TokenType.RBRACE);
|
|
277
|
+
return config;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private parsePhysicsRandomization(): DomainRandomizationConfig['physics'] {
|
|
281
|
+
this.expect(TokenType.LBRACE);
|
|
282
|
+
const physics: DomainRandomizationConfig['physics'] = {};
|
|
283
|
+
|
|
284
|
+
while (
|
|
285
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
286
|
+
this.currentToken().type !== TokenType.EOF
|
|
287
|
+
) {
|
|
288
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
289
|
+
this.expect(TokenType.COLON);
|
|
290
|
+
const value = this.parseValue();
|
|
291
|
+
|
|
292
|
+
if (key === 'massScale' && Array.isArray(value)) {
|
|
293
|
+
physics.massScale = [value[0] as number, value[1] as number];
|
|
294
|
+
} else if (key === 'frictionRange' && Array.isArray(value)) {
|
|
295
|
+
physics.frictionRange = [value[0] as number, value[1] as number];
|
|
296
|
+
} else if (key === 'dampingRange' && Array.isArray(value)) {
|
|
297
|
+
physics.dampingRange = [value[0] as number, value[1] as number];
|
|
298
|
+
} else if (key === 'armatureRange' && Array.isArray(value)) {
|
|
299
|
+
physics.armatureRange = [value[0] as number, value[1] as number];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.expect(TokenType.RBRACE);
|
|
304
|
+
return physics;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private parseActuatorRandomization(): DomainRandomizationConfig['actuator'] {
|
|
308
|
+
this.expect(TokenType.LBRACE);
|
|
309
|
+
const actuator: DomainRandomizationConfig['actuator'] = {};
|
|
310
|
+
|
|
311
|
+
while (
|
|
312
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
313
|
+
this.currentToken().type !== TokenType.EOF
|
|
314
|
+
) {
|
|
315
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
316
|
+
this.expect(TokenType.COLON);
|
|
317
|
+
const value = this.parseValue();
|
|
318
|
+
|
|
319
|
+
if (key === 'kpNoise' && typeof value === 'number') {
|
|
320
|
+
actuator.kpNoise = value;
|
|
321
|
+
} else if (key === 'kdNoise' && typeof value === 'number') {
|
|
322
|
+
actuator.kdNoise = value;
|
|
323
|
+
} else if (key === 'latencyNoise' && typeof value === 'number') {
|
|
324
|
+
actuator.latencyNoise = value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this.expect(TokenType.RBRACE);
|
|
329
|
+
return actuator;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private parseObservationRandomization(): DomainRandomizationConfig['observation'] {
|
|
333
|
+
this.expect(TokenType.LBRACE);
|
|
334
|
+
const observation: DomainRandomizationConfig['observation'] = {};
|
|
335
|
+
|
|
336
|
+
while (
|
|
337
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
338
|
+
this.currentToken().type !== TokenType.EOF
|
|
339
|
+
) {
|
|
340
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
341
|
+
this.expect(TokenType.COLON);
|
|
342
|
+
const value = this.parseValue();
|
|
343
|
+
|
|
344
|
+
if (key === 'jointPosNoise' && typeof value === 'number') {
|
|
345
|
+
observation.jointPosNoise = value;
|
|
346
|
+
} else if (key === 'jointVelNoise' && typeof value === 'number') {
|
|
347
|
+
observation.jointVelNoise = value;
|
|
348
|
+
} else if (key === 'imuNoise' && typeof value === 'number') {
|
|
349
|
+
observation.imuNoise = value;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.expect(TokenType.RBRACE);
|
|
354
|
+
return observation;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private parseInitialStateRandomization(): DomainRandomizationConfig['initialState'] {
|
|
358
|
+
this.expect(TokenType.LBRACE);
|
|
359
|
+
const initialState: DomainRandomizationConfig['initialState'] = {};
|
|
360
|
+
|
|
361
|
+
while (
|
|
362
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
363
|
+
this.currentToken().type !== TokenType.EOF
|
|
364
|
+
) {
|
|
365
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
366
|
+
this.expect(TokenType.COLON);
|
|
367
|
+
|
|
368
|
+
if (key === 'jointPosRange') {
|
|
369
|
+
initialState.jointPosRange = this.parseNumberRangeMap();
|
|
370
|
+
} else {
|
|
371
|
+
const value = this.parseValue();
|
|
372
|
+
if (key === 'rootPoseRange' && Array.isArray(value)) {
|
|
373
|
+
initialState.rootPoseRange = value as [number, number, number, number, number, number];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.expect(TokenType.RBRACE);
|
|
379
|
+
return initialState;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private parseNumberRangeMap(): Record<string, [number, number]> {
|
|
383
|
+
this.expect(TokenType.LBRACE);
|
|
384
|
+
const ranges: Record<string, [number, number]> = {};
|
|
385
|
+
|
|
386
|
+
while (
|
|
387
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
388
|
+
this.currentToken().type !== TokenType.EOF
|
|
389
|
+
) {
|
|
390
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
391
|
+
this.expect(TokenType.COLON);
|
|
392
|
+
const value = this.parseValue();
|
|
393
|
+
|
|
394
|
+
if (Array.isArray(value)) {
|
|
395
|
+
ranges[key] = [value[0] as number, value[1] as number];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this.expect(TokenType.RBRACE);
|
|
400
|
+
return ranges;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private parseDisturbanceRandomization(): DomainRandomizationConfig['disturbance'] {
|
|
404
|
+
this.expect(TokenType.LBRACE);
|
|
405
|
+
const disturbance: DomainRandomizationConfig['disturbance'] = {};
|
|
406
|
+
|
|
407
|
+
while (
|
|
408
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
409
|
+
this.currentToken().type !== TokenType.EOF
|
|
410
|
+
) {
|
|
411
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
412
|
+
this.expect(TokenType.COLON);
|
|
413
|
+
const value = this.parseValue();
|
|
414
|
+
|
|
415
|
+
if (key === 'forceRange' && Array.isArray(value)) {
|
|
416
|
+
disturbance.forceRange = [value[0] as number, value[1] as number];
|
|
417
|
+
} else if (key === 'intervalRange' && Array.isArray(value)) {
|
|
418
|
+
disturbance.intervalRange = [value[0] as number, value[1] as number];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.expect(TokenType.RBRACE);
|
|
423
|
+
return disturbance;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private parseActuatorGroupBlock(): ActuatorGroupConfig {
|
|
427
|
+
// Parse actuator_group { name: "foo" type: "DelayedPDActuator" joints: ["joint1", "joint2"] ... }
|
|
428
|
+
this.expect(TokenType.LBRACE);
|
|
429
|
+
const group: ActuatorGroupConfig = {
|
|
430
|
+
name: '',
|
|
431
|
+
type: 'IdealPDActuator',
|
|
432
|
+
jointNames: [],
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
while (
|
|
436
|
+
this.currentToken().type !== TokenType.RBRACE &&
|
|
437
|
+
this.currentToken().type !== TokenType.EOF
|
|
438
|
+
) {
|
|
439
|
+
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
440
|
+
this.expect(TokenType.COLON);
|
|
441
|
+
const value = this.parseValue();
|
|
442
|
+
|
|
443
|
+
switch (key) {
|
|
444
|
+
case 'name':
|
|
445
|
+
group.name = value as string;
|
|
446
|
+
break;
|
|
447
|
+
case 'type':
|
|
448
|
+
group.type = value as ActuatorGroupConfig['type'];
|
|
449
|
+
break;
|
|
450
|
+
case 'joints':
|
|
451
|
+
if (Array.isArray(value)) {
|
|
452
|
+
group.jointNames = value as string[];
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
case 'stiffness':
|
|
456
|
+
group.stiffness = value as number;
|
|
457
|
+
break;
|
|
458
|
+
case 'damping':
|
|
459
|
+
group.damping = value as number;
|
|
460
|
+
break;
|
|
461
|
+
case 'friction':
|
|
462
|
+
group.friction = value as number;
|
|
463
|
+
break;
|
|
464
|
+
case 'latency':
|
|
465
|
+
group.latency = value as number;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
this.expect(TokenType.RBRACE);
|
|
471
|
+
return group;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** @ros2_hardware_loop Trait — Bidirectional hardware-in-the-loop bridge for physical robotics. @trait ros2_hardware_loop */
|
|
2
|
+
export interface ROS2Config {
|
|
3
|
+
nodeName: string;
|
|
4
|
+
topicPrefix: string;
|
|
5
|
+
updateFrequencyHz: number;
|
|
6
|
+
bidirectional: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ROS2State {
|
|
10
|
+
connected: boolean;
|
|
11
|
+
lastPingMs: number;
|
|
12
|
+
activeTopics: string[];
|
|
13
|
+
hardwareSyncDriftMs: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
17
|
+
|
|
18
|
+
export function createROS2HardwareLoopHandler(): TraitHandler<ROS2Config> {
|
|
19
|
+
return {
|
|
20
|
+
name: 'ros2_hardware_loop',
|
|
21
|
+
defaultConfig: {
|
|
22
|
+
nodeName: 'holo_rig_01',
|
|
23
|
+
topicPrefix: '/holo',
|
|
24
|
+
updateFrequencyHz: 60,
|
|
25
|
+
bidirectional: true,
|
|
26
|
+
},
|
|
27
|
+
onAttach(n: unknown, c: ROS2Config, ctx: unknown) {
|
|
28
|
+
(n as any).__ros2State = {
|
|
29
|
+
connected: false,
|
|
30
|
+
lastPingMs: 0,
|
|
31
|
+
activeTopics: [],
|
|
32
|
+
hardwareSyncDriftMs: 0,
|
|
33
|
+
};
|
|
34
|
+
(ctx as any).emit?.('ros2:init', { node: c.nodeName });
|
|
35
|
+
},
|
|
36
|
+
onDetach(n: unknown, _c: ROS2Config, ctx: unknown) {
|
|
37
|
+
delete (n as any).__ros2State;
|
|
38
|
+
(ctx as any).emit?.('ros2:disconnect');
|
|
39
|
+
},
|
|
40
|
+
onUpdate() {},
|
|
41
|
+
onEvent(n: unknown, c: ROS2Config, ctx: unknown, e: unknown) {
|
|
42
|
+
const s = (n as any).__ros2State as ROS2State;
|
|
43
|
+
if (!s) return;
|
|
44
|
+
const evt = e as any;
|
|
45
|
+
|
|
46
|
+
switch (evt.type) {
|
|
47
|
+
case 'ros2:connect':
|
|
48
|
+
s.connected = true;
|
|
49
|
+
// In real usage, initializes rclnodejs bridge for pub/sub physics
|
|
50
|
+
(ctx as any).emit?.('ros2:connected', { latencyMs: 12 });
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case 'scene:transform_change':
|
|
54
|
+
if (c.bidirectional && s.connected) {
|
|
55
|
+
// Push virtual set transform to physical robot joint
|
|
56
|
+
(ctx as any).emit?.('ros2:publish', {
|
|
57
|
+
topic: `${c.topicPrefix}/joint_cmd`,
|
|
58
|
+
payload: evt.payload,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case 'ros2:telemetry':
|
|
64
|
+
// Receive physical robot telemetry to drive virtual set
|
|
65
|
+
s.hardwareSyncDriftMs = Math.abs(Date.now() - (evt.payload.timestamp || Date.now()));
|
|
66
|
+
(n as any).transform = evt.payload.transform;
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'ttu:manifested':
|
|
70
|
+
// Dynamically bind to Text-To-Universe AST streams
|
|
71
|
+
const crdtRoot = evt.payload?.crdtRoot;
|
|
72
|
+
if (crdtRoot && s.connected) {
|
|
73
|
+
const dynamicTopic = `${c.topicPrefix}/ttu_sync/${crdtRoot}`;
|
|
74
|
+
s.activeTopics.push(dynamicTopic);
|
|
75
|
+
(ctx as any).emit?.('ros2:subscribe', { topic: dynamicTopic });
|
|
76
|
+
(ctx as any).emit?.('ros2:log', {
|
|
77
|
+
message: `Bridged digital twin for TTU root: ${crdtRoot}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|