@holoscript/robotics-plugin 2.0.2 → 2.0.3

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/dist/lexer.js ADDED
@@ -0,0 +1,251 @@
1
+ /**
2
+ * HoloScript Lexer (Tokenizer)
3
+ *
4
+ * Converts HoloScript source code into a stream of tokens for parsing.
5
+ * Supports: keywords, identifiers, traits (@trait), strings, numbers, symbols
6
+ */
7
+ export var TokenType;
8
+ (function (TokenType) {
9
+ // Keywords
10
+ TokenType["KEYWORD"] = "KEYWORD";
11
+ // Identifiers & Traits
12
+ TokenType["IDENTIFIER"] = "IDENTIFIER";
13
+ TokenType["TRAIT"] = "TRAIT";
14
+ TokenType["STRING"] = "STRING";
15
+ TokenType["NUMBER"] = "NUMBER";
16
+ // Symbols
17
+ TokenType["LBRACE"] = "LBRACE";
18
+ TokenType["RBRACE"] = "RBRACE";
19
+ TokenType["LBRACKET"] = "LBRACKET";
20
+ TokenType["RBRACKET"] = "RBRACKET";
21
+ TokenType["COLON"] = "COLON";
22
+ TokenType["COMMA"] = "COMMA";
23
+ // Special
24
+ TokenType["COMMENT"] = "COMMENT";
25
+ TokenType["EOF"] = "EOF";
26
+ })(TokenType || (TokenType = {}));
27
+ export class Lexer {
28
+ constructor(source) {
29
+ this.position = 0;
30
+ this.line = 1;
31
+ this.column = 1;
32
+ this.tokens = [];
33
+ this.keywords = new Set([
34
+ 'composition',
35
+ 'object',
36
+ 'using',
37
+ 'template',
38
+ 'group',
39
+ 'scene',
40
+ ]);
41
+ this.source = source;
42
+ }
43
+ tokenize() {
44
+ while (this.position < this.source.length) {
45
+ this.scanToken();
46
+ }
47
+ this.tokens.push({
48
+ type: TokenType.EOF,
49
+ value: '',
50
+ line: this.line,
51
+ column: this.column,
52
+ });
53
+ return this.tokens;
54
+ }
55
+ scanToken() {
56
+ const char = this.currentChar();
57
+ // Skip whitespace
58
+ if (this.isWhitespace(char)) {
59
+ this.skipWhitespace();
60
+ return;
61
+ }
62
+ // Comments
63
+ if (char === '/' && this.peek() === '/') {
64
+ this.skipComment();
65
+ return;
66
+ }
67
+ // Traits (@identifier)
68
+ if (char === '@') {
69
+ this.scanTrait();
70
+ return;
71
+ }
72
+ // String literals ("...")
73
+ if (char === '"') {
74
+ this.scanString();
75
+ return;
76
+ }
77
+ // Numbers (0.1, 3.14, -2.0)
78
+ if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek()))) {
79
+ this.scanNumber();
80
+ return;
81
+ }
82
+ // Keywords & Identifiers
83
+ if (this.isAlpha(char)) {
84
+ this.scanIdentifierOrKeyword();
85
+ return;
86
+ }
87
+ // Single-character tokens
88
+ this.scanSymbol(char);
89
+ }
90
+ scanTrait() {
91
+ const startLine = this.line;
92
+ const startColumn = this.column;
93
+ this.advance(); // Skip '@'
94
+ let trait = '';
95
+ while (this.position < this.source.length &&
96
+ this.isAlphaNumericOrUnderscore(this.currentChar())) {
97
+ trait += this.currentChar();
98
+ this.advance();
99
+ }
100
+ this.tokens.push({
101
+ type: TokenType.TRAIT,
102
+ value: trait,
103
+ line: startLine,
104
+ column: startColumn,
105
+ });
106
+ }
107
+ scanString() {
108
+ const startLine = this.line;
109
+ const startColumn = this.column;
110
+ this.advance(); // Skip opening "
111
+ let str = '';
112
+ while (this.position < this.source.length && this.currentChar() !== '"') {
113
+ if (this.currentChar() === '\\') {
114
+ // Handle escape sequences
115
+ this.advance();
116
+ if (this.position < this.source.length) {
117
+ str += this.currentChar();
118
+ this.advance();
119
+ }
120
+ }
121
+ else {
122
+ str += this.currentChar();
123
+ this.advance();
124
+ }
125
+ }
126
+ if (this.currentChar() === '"') {
127
+ this.advance(); // Skip closing "
128
+ }
129
+ else {
130
+ throw new Error(`Unterminated string at line ${startLine}, column ${startColumn}`);
131
+ }
132
+ this.tokens.push({
133
+ type: TokenType.STRING,
134
+ value: str,
135
+ line: startLine,
136
+ column: startColumn,
137
+ });
138
+ }
139
+ scanNumber() {
140
+ const startLine = this.line;
141
+ const startColumn = this.column;
142
+ let num = '';
143
+ // Handle negative sign
144
+ if (this.currentChar() === '-') {
145
+ num += this.currentChar();
146
+ this.advance();
147
+ }
148
+ // Integer part
149
+ while (this.position < this.source.length && this.isDigit(this.currentChar())) {
150
+ num += this.currentChar();
151
+ this.advance();
152
+ }
153
+ // Decimal part
154
+ if (this.currentChar() === '.' && this.isDigit(this.peek())) {
155
+ num += this.currentChar();
156
+ this.advance();
157
+ while (this.position < this.source.length && this.isDigit(this.currentChar())) {
158
+ num += this.currentChar();
159
+ this.advance();
160
+ }
161
+ }
162
+ this.tokens.push({
163
+ type: TokenType.NUMBER,
164
+ value: num,
165
+ line: startLine,
166
+ column: startColumn,
167
+ });
168
+ }
169
+ scanIdentifierOrKeyword() {
170
+ const startLine = this.line;
171
+ const startColumn = this.column;
172
+ let ident = '';
173
+ while (this.position < this.source.length &&
174
+ this.isAlphaNumericOrUnderscore(this.currentChar())) {
175
+ ident += this.currentChar();
176
+ this.advance();
177
+ }
178
+ const type = this.keywords.has(ident) ? TokenType.KEYWORD : TokenType.IDENTIFIER;
179
+ this.tokens.push({
180
+ type,
181
+ value: ident,
182
+ line: startLine,
183
+ column: startColumn,
184
+ });
185
+ }
186
+ scanSymbol(char) {
187
+ const symbolMap = {
188
+ '{': TokenType.LBRACE,
189
+ '}': TokenType.RBRACE,
190
+ '[': TokenType.LBRACKET,
191
+ ']': TokenType.RBRACKET,
192
+ ':': TokenType.COLON,
193
+ ',': TokenType.COMMA,
194
+ };
195
+ const tokenType = symbolMap[char];
196
+ if (tokenType) {
197
+ this.tokens.push({
198
+ type: tokenType,
199
+ value: char,
200
+ line: this.line,
201
+ column: this.column,
202
+ });
203
+ this.advance();
204
+ }
205
+ else {
206
+ throw new Error(`Unexpected character '${char}' at line ${this.line}, column ${this.column}`);
207
+ }
208
+ }
209
+ skipWhitespace() {
210
+ while (this.position < this.source.length && this.isWhitespace(this.currentChar())) {
211
+ if (this.currentChar() === '\n') {
212
+ this.line++;
213
+ this.column = 1;
214
+ }
215
+ else {
216
+ this.column++;
217
+ }
218
+ this.position++;
219
+ }
220
+ }
221
+ skipComment() {
222
+ // Skip until end of line
223
+ while (this.position < this.source.length && this.currentChar() !== '\n') {
224
+ this.position++;
225
+ this.column++;
226
+ }
227
+ }
228
+ currentChar() {
229
+ return this.source[this.position] || '';
230
+ }
231
+ peek(offset = 1) {
232
+ const peekPos = this.position + offset;
233
+ return this.source[peekPos] || '';
234
+ }
235
+ advance() {
236
+ this.position++;
237
+ this.column++;
238
+ }
239
+ isWhitespace(char) {
240
+ return /\s/.test(char);
241
+ }
242
+ isDigit(char) {
243
+ return /[0-9]/.test(char);
244
+ }
245
+ isAlpha(char) {
246
+ return /[a-z_]/i.test(char);
247
+ }
248
+ isAlphaNumericOrUnderscore(char) {
249
+ return /[a-z0-9_]/i.test(char);
250
+ }
251
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * HoloScript Parser
3
+ *
4
+ * Converts token stream into Abstract Syntax Tree (AST).
5
+ * Uses recursive descent parsing.
6
+ */
7
+ import { Token } from './lexer.js';
8
+ import { CompositionNode } from './ast.js';
9
+ export declare class Parser {
10
+ private tokens;
11
+ private position;
12
+ constructor(tokens: Token[]);
13
+ parse(): CompositionNode;
14
+ private currentToken;
15
+ private peek;
16
+ private advance;
17
+ private expect;
18
+ private parseComposition;
19
+ private parseObject;
20
+ private parseValue;
21
+ private parseArray;
22
+ private parseDomainRandomizationBlock;
23
+ private parsePhysicsRandomization;
24
+ private parseActuatorRandomization;
25
+ private parseObservationRandomization;
26
+ private parseInitialStateRandomization;
27
+ private parseNumberRangeMap;
28
+ private parseDisturbanceRandomization;
29
+ private parseActuatorGroupBlock;
30
+ }
package/dist/parser.js ADDED
@@ -0,0 +1,376 @@
1
+ /**
2
+ * HoloScript Parser
3
+ *
4
+ * Converts token stream into Abstract Syntax Tree (AST).
5
+ * Uses recursive descent parsing.
6
+ */
7
+ import { TokenType } from './lexer.js';
8
+ export class Parser {
9
+ constructor(tokens) {
10
+ this.position = 0;
11
+ this.tokens = tokens;
12
+ }
13
+ parse() {
14
+ return this.parseComposition();
15
+ }
16
+ currentToken() {
17
+ const token = this.tokens[this.position];
18
+ if (!token) {
19
+ throw new Error('Unexpected end of input');
20
+ }
21
+ return token;
22
+ }
23
+ peek(offset = 1) {
24
+ const pos = this.position + offset;
25
+ const token = pos < this.tokens.length ? this.tokens[pos] : this.tokens[this.tokens.length - 1];
26
+ if (!token) {
27
+ throw new Error('Unexpected end of input');
28
+ }
29
+ return token;
30
+ }
31
+ advance() {
32
+ const token = this.currentToken();
33
+ this.position++;
34
+ return token;
35
+ }
36
+ expect(type, value) {
37
+ const token = this.currentToken();
38
+ if (token.type !== type) {
39
+ throw new Error(`Expected ${type}${value ? ` "${value}"` : ''} but got ${token.type} "${token.value}" at line ${token.line}, column ${token.column}`);
40
+ }
41
+ if (value !== undefined && token.value !== value) {
42
+ throw new Error(`Expected "${value}" but got "${token.value}" at line ${token.line}, column ${token.column}`);
43
+ }
44
+ return this.advance();
45
+ }
46
+ parseComposition() {
47
+ const startToken = this.expect(TokenType.KEYWORD, 'composition');
48
+ const name = this.expect(TokenType.STRING).value;
49
+ this.expect(TokenType.LBRACE);
50
+ const objects = [];
51
+ let domainRandomization;
52
+ while (this.currentToken().type !== TokenType.RBRACE &&
53
+ this.currentToken().type !== TokenType.EOF) {
54
+ // Check for domain_randomization block at composition level
55
+ if (this.currentToken().type === TokenType.IDENTIFIER &&
56
+ this.currentToken().value === 'domain_randomization') {
57
+ this.advance();
58
+ this.expect(TokenType.COLON);
59
+ domainRandomization = this.parseDomainRandomizationBlock();
60
+ }
61
+ else {
62
+ objects.push(this.parseObject());
63
+ }
64
+ }
65
+ this.expect(TokenType.RBRACE);
66
+ return {
67
+ type: 'composition',
68
+ name,
69
+ objects,
70
+ domainRandomization,
71
+ line: startToken.line,
72
+ column: startToken.column,
73
+ };
74
+ }
75
+ parseObject() {
76
+ const startToken = this.expect(TokenType.KEYWORD, 'object');
77
+ const name = this.expect(TokenType.STRING).value;
78
+ // Parse traits (@trait1 @trait2 ...)
79
+ const traits = [];
80
+ while (this.currentToken().type === TokenType.TRAIT) {
81
+ traits.push(this.advance().value);
82
+ }
83
+ this.expect(TokenType.LBRACE);
84
+ // Parse properties (key: value)
85
+ const properties = {};
86
+ let domainRandomization;
87
+ let actuatorGroups;
88
+ while (this.currentToken().type !== TokenType.RBRACE &&
89
+ this.currentToken().type !== TokenType.EOF) {
90
+ // Support both object "link" @trait { ... } and object "link" { @trait ... }.
91
+ if (this.currentToken().type === TokenType.TRAIT) {
92
+ const trait = this.advance().value;
93
+ if (!traits.includes(trait)) {
94
+ traits.push(trait);
95
+ }
96
+ }
97
+ // Check for domain_randomization block
98
+ else if (this.currentToken().type === TokenType.IDENTIFIER &&
99
+ this.currentToken().value === 'domain_randomization') {
100
+ this.advance();
101
+ this.expect(TokenType.COLON);
102
+ domainRandomization = this.parseDomainRandomizationBlock();
103
+ }
104
+ // Check for actuator_group block
105
+ else if (this.currentToken().type === TokenType.IDENTIFIER &&
106
+ this.currentToken().value === 'actuator_group') {
107
+ this.advance();
108
+ this.expect(TokenType.COLON);
109
+ const group = this.parseActuatorGroupBlock();
110
+ if (!actuatorGroups)
111
+ actuatorGroups = [];
112
+ actuatorGroups.push(group);
113
+ }
114
+ // Regular property
115
+ else {
116
+ const keyName = this.expect(TokenType.IDENTIFIER).value;
117
+ this.expect(TokenType.COLON);
118
+ const value = this.parseValue();
119
+ properties[keyName] = value;
120
+ }
121
+ }
122
+ this.expect(TokenType.RBRACE);
123
+ return {
124
+ type: 'object',
125
+ name,
126
+ traits,
127
+ properties,
128
+ domainRandomization,
129
+ actuatorGroups,
130
+ line: startToken.line,
131
+ column: startToken.column,
132
+ };
133
+ }
134
+ parseValue() {
135
+ const token = this.currentToken();
136
+ // String value
137
+ if (token.type === TokenType.STRING) {
138
+ return this.advance().value;
139
+ }
140
+ // Number value
141
+ if (token.type === TokenType.NUMBER) {
142
+ return parseFloat(this.advance().value);
143
+ }
144
+ // Array value [1, 2, 3]
145
+ if (token.type === TokenType.LBRACKET) {
146
+ return this.parseArray();
147
+ }
148
+ // Function call (for future: diagonal([1, 2, 3]))
149
+ if (token.type === TokenType.IDENTIFIER && this.peek().type === TokenType.LBRACKET) {
150
+ this.advance(); // Skip function name for now
151
+ const args = this.parseArray();
152
+ // For now, treat function calls as arrays (e.g., diagonal([1,2,3]) -> [1,2,3])
153
+ // In future, could preserve function name metadata
154
+ return args;
155
+ }
156
+ throw new Error(`Unexpected value type ${token.type} "${token.value}" at line ${token.line}, column ${token.column}`);
157
+ }
158
+ parseArray() {
159
+ this.expect(TokenType.LBRACKET);
160
+ const values = [];
161
+ while (this.currentToken().type !== TokenType.RBRACKET &&
162
+ this.currentToken().type !== TokenType.EOF) {
163
+ if (this.currentToken().type === TokenType.NUMBER) {
164
+ values.push(parseFloat(this.advance().value));
165
+ }
166
+ else if (this.currentToken().type === TokenType.STRING) {
167
+ values.push(this.advance().value);
168
+ }
169
+ else if (this.currentToken().type === TokenType.LBRACKET) {
170
+ // Nested array
171
+ values.push(this.parseArray());
172
+ }
173
+ else {
174
+ throw new Error(`Unexpected token in array: ${this.currentToken().type} at line ${this.currentToken().line}`);
175
+ }
176
+ // Optional comma
177
+ if (this.currentToken().type === TokenType.COMMA) {
178
+ this.advance();
179
+ }
180
+ }
181
+ this.expect(TokenType.RBRACKET);
182
+ return values;
183
+ }
184
+ parseDomainRandomizationBlock() {
185
+ this.expect(TokenType.LBRACE);
186
+ const config = {};
187
+ while (this.currentToken().type !== TokenType.RBRACE &&
188
+ this.currentToken().type !== TokenType.EOF) {
189
+ const key = this.expect(TokenType.IDENTIFIER).value;
190
+ this.expect(TokenType.COLON);
191
+ switch (key) {
192
+ case 'physics':
193
+ config.physics = this.parsePhysicsRandomization();
194
+ break;
195
+ case 'actuator':
196
+ config.actuator = this.parseActuatorRandomization();
197
+ break;
198
+ case 'observation':
199
+ config.observation = this.parseObservationRandomization();
200
+ break;
201
+ case 'initialState':
202
+ config.initialState = this.parseInitialStateRandomization();
203
+ break;
204
+ case 'disturbance':
205
+ config.disturbance = this.parseDisturbanceRandomization();
206
+ break;
207
+ default:
208
+ // Skip unknown keys
209
+ this.parseValue();
210
+ }
211
+ }
212
+ this.expect(TokenType.RBRACE);
213
+ return config;
214
+ }
215
+ parsePhysicsRandomization() {
216
+ this.expect(TokenType.LBRACE);
217
+ const physics = {};
218
+ while (this.currentToken().type !== TokenType.RBRACE &&
219
+ this.currentToken().type !== TokenType.EOF) {
220
+ const key = this.expect(TokenType.IDENTIFIER).value;
221
+ this.expect(TokenType.COLON);
222
+ const value = this.parseValue();
223
+ if (key === 'massScale' && Array.isArray(value)) {
224
+ physics.massScale = [value[0], value[1]];
225
+ }
226
+ else if (key === 'frictionRange' && Array.isArray(value)) {
227
+ physics.frictionRange = [value[0], value[1]];
228
+ }
229
+ else if (key === 'dampingRange' && Array.isArray(value)) {
230
+ physics.dampingRange = [value[0], value[1]];
231
+ }
232
+ else if (key === 'armatureRange' && Array.isArray(value)) {
233
+ physics.armatureRange = [value[0], value[1]];
234
+ }
235
+ }
236
+ this.expect(TokenType.RBRACE);
237
+ return physics;
238
+ }
239
+ parseActuatorRandomization() {
240
+ this.expect(TokenType.LBRACE);
241
+ const actuator = {};
242
+ while (this.currentToken().type !== TokenType.RBRACE &&
243
+ this.currentToken().type !== TokenType.EOF) {
244
+ const key = this.expect(TokenType.IDENTIFIER).value;
245
+ this.expect(TokenType.COLON);
246
+ const value = this.parseValue();
247
+ if (key === 'kpNoise' && typeof value === 'number') {
248
+ actuator.kpNoise = value;
249
+ }
250
+ else if (key === 'kdNoise' && typeof value === 'number') {
251
+ actuator.kdNoise = value;
252
+ }
253
+ else if (key === 'latencyNoise' && typeof value === 'number') {
254
+ actuator.latencyNoise = value;
255
+ }
256
+ }
257
+ this.expect(TokenType.RBRACE);
258
+ return actuator;
259
+ }
260
+ parseObservationRandomization() {
261
+ this.expect(TokenType.LBRACE);
262
+ const observation = {};
263
+ while (this.currentToken().type !== TokenType.RBRACE &&
264
+ this.currentToken().type !== TokenType.EOF) {
265
+ const key = this.expect(TokenType.IDENTIFIER).value;
266
+ this.expect(TokenType.COLON);
267
+ const value = this.parseValue();
268
+ if (key === 'jointPosNoise' && typeof value === 'number') {
269
+ observation.jointPosNoise = value;
270
+ }
271
+ else if (key === 'jointVelNoise' && typeof value === 'number') {
272
+ observation.jointVelNoise = value;
273
+ }
274
+ else if (key === 'imuNoise' && typeof value === 'number') {
275
+ observation.imuNoise = value;
276
+ }
277
+ }
278
+ this.expect(TokenType.RBRACE);
279
+ return observation;
280
+ }
281
+ parseInitialStateRandomization() {
282
+ this.expect(TokenType.LBRACE);
283
+ const initialState = {};
284
+ while (this.currentToken().type !== TokenType.RBRACE &&
285
+ this.currentToken().type !== TokenType.EOF) {
286
+ const key = this.expect(TokenType.IDENTIFIER).value;
287
+ this.expect(TokenType.COLON);
288
+ if (key === 'jointPosRange') {
289
+ initialState.jointPosRange = this.parseNumberRangeMap();
290
+ }
291
+ else {
292
+ const value = this.parseValue();
293
+ if (key === 'rootPoseRange' && Array.isArray(value)) {
294
+ initialState.rootPoseRange = value;
295
+ }
296
+ }
297
+ }
298
+ this.expect(TokenType.RBRACE);
299
+ return initialState;
300
+ }
301
+ parseNumberRangeMap() {
302
+ this.expect(TokenType.LBRACE);
303
+ const ranges = {};
304
+ while (this.currentToken().type !== TokenType.RBRACE &&
305
+ this.currentToken().type !== TokenType.EOF) {
306
+ const key = this.expect(TokenType.IDENTIFIER).value;
307
+ this.expect(TokenType.COLON);
308
+ const value = this.parseValue();
309
+ if (Array.isArray(value)) {
310
+ ranges[key] = [value[0], value[1]];
311
+ }
312
+ }
313
+ this.expect(TokenType.RBRACE);
314
+ return ranges;
315
+ }
316
+ parseDisturbanceRandomization() {
317
+ this.expect(TokenType.LBRACE);
318
+ const disturbance = {};
319
+ while (this.currentToken().type !== TokenType.RBRACE &&
320
+ this.currentToken().type !== TokenType.EOF) {
321
+ const key = this.expect(TokenType.IDENTIFIER).value;
322
+ this.expect(TokenType.COLON);
323
+ const value = this.parseValue();
324
+ if (key === 'forceRange' && Array.isArray(value)) {
325
+ disturbance.forceRange = [value[0], value[1]];
326
+ }
327
+ else if (key === 'intervalRange' && Array.isArray(value)) {
328
+ disturbance.intervalRange = [value[0], value[1]];
329
+ }
330
+ }
331
+ this.expect(TokenType.RBRACE);
332
+ return disturbance;
333
+ }
334
+ parseActuatorGroupBlock() {
335
+ // Parse actuator_group { name: "foo" type: "DelayedPDActuator" joints: ["joint1", "joint2"] ... }
336
+ this.expect(TokenType.LBRACE);
337
+ const group = {
338
+ name: '',
339
+ type: 'IdealPDActuator',
340
+ jointNames: [],
341
+ };
342
+ while (this.currentToken().type !== TokenType.RBRACE &&
343
+ this.currentToken().type !== TokenType.EOF) {
344
+ const key = this.expect(TokenType.IDENTIFIER).value;
345
+ this.expect(TokenType.COLON);
346
+ const value = this.parseValue();
347
+ switch (key) {
348
+ case 'name':
349
+ group.name = value;
350
+ break;
351
+ case 'type':
352
+ group.type = value;
353
+ break;
354
+ case 'joints':
355
+ if (Array.isArray(value)) {
356
+ group.jointNames = value;
357
+ }
358
+ break;
359
+ case 'stiffness':
360
+ group.stiffness = value;
361
+ break;
362
+ case 'damping':
363
+ group.damping = value;
364
+ break;
365
+ case 'friction':
366
+ group.friction = value;
367
+ break;
368
+ case 'latency':
369
+ group.latency = value;
370
+ break;
371
+ }
372
+ }
373
+ this.expect(TokenType.RBRACE);
374
+ return group;
375
+ }
376
+ }
@@ -0,0 +1,5 @@
1
+ export declare const ROBOTICS_PLUGIN_ID: "robotics";
2
+ export interface TraitRegistrar {
3
+ registerTrait(name: string, handler: unknown): void;
4
+ }
5
+ export declare function registerRoboticsTraitHandlers(registrar: TraitRegistrar): void;
@@ -0,0 +1,6 @@
1
+ import { registerPluginTraits } from '@holoscript/core/runtime';
2
+ import { createROS2HardwareLoopHandler } from './traits/ROS2HardwareLoopTrait.js';
3
+ export const ROBOTICS_PLUGIN_ID = 'robotics';
4
+ export function registerRoboticsTraitHandlers(registrar) {
5
+ registerPluginTraits(registrar, ROBOTICS_PLUGIN_ID, [createROS2HardwareLoopHandler()]);
6
+ }
@@ -0,0 +1,15 @@
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
+ export interface ROS2State {
9
+ connected: boolean;
10
+ lastPingMs: number;
11
+ activeTopics: string[];
12
+ hardwareSyncDriftMs: number;
13
+ }
14
+ import type { TraitHandler } from './types.js';
15
+ export declare function createROS2HardwareLoopHandler(): TraitHandler<ROS2Config>;