rclnodejs 1.7.0 → 1.8.0
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/binding.gyp +2 -0
- package/index.js +93 -0
- package/lib/action/client.js +54 -1
- package/lib/client.js +66 -1
- package/lib/clock.js +178 -0
- package/lib/clock_change.js +49 -0
- package/lib/clock_event.js +88 -0
- package/lib/errors.js +50 -0
- package/lib/logging.js +78 -0
- package/lib/message_introspector.js +123 -0
- package/lib/message_validation.js +512 -0
- package/lib/node.js +133 -1
- package/lib/node_options.js +40 -1
- package/lib/observable_subscription.js +105 -0
- package/lib/publisher.js +56 -1
- package/lib/qos.js +57 -0
- package/lib/subscription.js +8 -0
- package/lib/timer.js +42 -0
- package/lib/validator.js +63 -7
- package/package.json +4 -2
- package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
- package/rosidl_gen/message_translator.js +0 -61
- package/scripts/config.js +1 -0
- package/src/addon.cpp +2 -0
- package/src/clock_event.cpp +268 -0
- package/src/clock_event.hpp +62 -0
- package/src/macros.h +2 -4
- package/src/rcl_action_server_bindings.cpp +21 -3
- package/src/rcl_bindings.cpp +59 -0
- package/src/rcl_context_bindings.cpp +5 -0
- package/src/rcl_graph_bindings.cpp +73 -0
- package/src/rcl_logging_bindings.cpp +158 -0
- package/src/rcl_node_bindings.cpp +14 -2
- package/src/rcl_publisher_bindings.cpp +12 -0
- package/src/rcl_service_bindings.cpp +7 -6
- package/src/rcl_subscription_bindings.cpp +51 -14
- package/src/rcl_time_point_bindings.cpp +135 -0
- package/src/rcl_timer_bindings.cpp +140 -0
- package/src/rcl_utilities.cpp +103 -2
- package/src/rcl_utilities.h +7 -1
- package/types/action_client.d.ts +27 -2
- package/types/base.d.ts +3 -0
- package/types/client.d.ts +29 -1
- package/types/clock.d.ts +86 -0
- package/types/clock_change.d.ts +27 -0
- package/types/clock_event.d.ts +51 -0
- package/types/errors.d.ts +49 -0
- package/types/index.d.ts +10 -0
- package/types/interfaces.d.ts +1 -1910
- package/types/logging.d.ts +32 -0
- package/types/message_introspector.d.ts +75 -0
- package/types/message_validation.d.ts +183 -0
- package/types/node.d.ts +67 -0
- package/types/node_options.d.ts +13 -0
- package/types/observable_subscription.d.ts +39 -0
- package/types/publisher.d.ts +28 -1
- package/types/qos.d.ts +18 -0
- package/types/subscription.d.ts +6 -0
- package/types/timer.d.ts +18 -0
- package/types/validator.d.ts +86 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { MessageValidationError, TypeValidationError } = require('./errors.js');
|
|
18
|
+
const interfaceLoader = require('./interface_loader.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validation issue problem types
|
|
22
|
+
* @enum {string}
|
|
23
|
+
*/
|
|
24
|
+
const ValidationProblem = {
|
|
25
|
+
/** Field exists in object but not in message schema */
|
|
26
|
+
UNKNOWN_FIELD: 'UNKNOWN_FIELD',
|
|
27
|
+
/** Field type doesn't match expected type */
|
|
28
|
+
TYPE_MISMATCH: 'TYPE_MISMATCH',
|
|
29
|
+
/** Required field is missing */
|
|
30
|
+
MISSING_FIELD: 'MISSING_FIELD',
|
|
31
|
+
/** Array length constraint violated */
|
|
32
|
+
ARRAY_LENGTH: 'ARRAY_LENGTH',
|
|
33
|
+
/** Value is out of valid range */
|
|
34
|
+
OUT_OF_RANGE: 'OUT_OF_RANGE',
|
|
35
|
+
/** Nested message validation failed */
|
|
36
|
+
NESTED_ERROR: 'NESTED_ERROR',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Map ROS primitive types to JavaScript types
|
|
41
|
+
*/
|
|
42
|
+
const PRIMITIVE_TYPE_MAP = {
|
|
43
|
+
bool: 'boolean',
|
|
44
|
+
int8: 'number',
|
|
45
|
+
uint8: 'number',
|
|
46
|
+
int16: 'number',
|
|
47
|
+
uint16: 'number',
|
|
48
|
+
int32: 'number',
|
|
49
|
+
uint32: 'number',
|
|
50
|
+
int64: 'bigint',
|
|
51
|
+
uint64: 'bigint',
|
|
52
|
+
float32: 'number',
|
|
53
|
+
float64: 'number',
|
|
54
|
+
char: 'number',
|
|
55
|
+
byte: 'number',
|
|
56
|
+
string: 'string',
|
|
57
|
+
wstring: 'string',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if value is a TypedArray
|
|
62
|
+
* @param {any} value - Value to check
|
|
63
|
+
* @returns {boolean} True if TypedArray
|
|
64
|
+
*/
|
|
65
|
+
function isTypedArray(value) {
|
|
66
|
+
return ArrayBuffer.isView(value) && !(value instanceof DataView);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the JavaScript type string for a value
|
|
71
|
+
* @param {any} value - Value to get type of
|
|
72
|
+
* @returns {string} Type description
|
|
73
|
+
*/
|
|
74
|
+
function getValueType(value) {
|
|
75
|
+
if (value === null) return 'null';
|
|
76
|
+
if (value === undefined) return 'undefined';
|
|
77
|
+
if (Array.isArray(value)) return 'array';
|
|
78
|
+
if (isTypedArray(value)) return 'TypedArray';
|
|
79
|
+
return typeof value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a type class from various input formats
|
|
84
|
+
* @param {string|object|function} typeClass - Type identifier
|
|
85
|
+
* @returns {function|null} The resolved type class or null
|
|
86
|
+
*/
|
|
87
|
+
function resolveTypeClass(typeClass) {
|
|
88
|
+
if (typeof typeClass === 'function') {
|
|
89
|
+
return typeClass;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
return interfaceLoader.loadInterface(typeClass);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get message type string from type class
|
|
101
|
+
* @param {function} typeClass - Message type class
|
|
102
|
+
* @returns {string} Message type string (e.g., 'std_msgs/msg/String')
|
|
103
|
+
*/
|
|
104
|
+
function getMessageTypeString(typeClass) {
|
|
105
|
+
if (typeof typeClass.type === 'function') {
|
|
106
|
+
const t = typeClass.type();
|
|
107
|
+
return `${t.pkgName}/${t.subFolder}/${t.interfaceName}`;
|
|
108
|
+
}
|
|
109
|
+
return 'unknown';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the schema definition for a message type
|
|
114
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
115
|
+
* @returns {object|null} Schema definition with fields and constants, or null if not found
|
|
116
|
+
* @example
|
|
117
|
+
* const schema = getMessageSchema(StringClass);
|
|
118
|
+
* // Returns: {
|
|
119
|
+
* // fields: [{name: 'data', type: {type: 'string', isPrimitiveType: true, ...}}],
|
|
120
|
+
* // constants: [],
|
|
121
|
+
* // messageType: 'std_msgs/msg/String'
|
|
122
|
+
* // }
|
|
123
|
+
*/
|
|
124
|
+
function getMessageSchema(typeClass) {
|
|
125
|
+
const resolved = resolveTypeClass(typeClass);
|
|
126
|
+
if (!resolved || !resolved.ROSMessageDef) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const def = resolved.ROSMessageDef;
|
|
131
|
+
return {
|
|
132
|
+
fields: def.fields || [],
|
|
133
|
+
constants: def.constants || [],
|
|
134
|
+
messageType: getMessageTypeString(resolved),
|
|
135
|
+
baseType: def.baseType,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get field names for a message type
|
|
141
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
142
|
+
* @returns {string[]} Array of field names
|
|
143
|
+
*/
|
|
144
|
+
function getFieldNames(typeClass) {
|
|
145
|
+
const schema = getMessageSchema(typeClass);
|
|
146
|
+
if (!schema) return [];
|
|
147
|
+
return schema.fields.map((f) => f.name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get type information for a specific field
|
|
152
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
153
|
+
* @param {string} fieldName - Name of the field
|
|
154
|
+
* @returns {object|null} Field type information or null if not found
|
|
155
|
+
*/
|
|
156
|
+
function getFieldType(typeClass, fieldName) {
|
|
157
|
+
const schema = getMessageSchema(typeClass);
|
|
158
|
+
if (!schema) return null;
|
|
159
|
+
|
|
160
|
+
const field = schema.fields.find((f) => f.name === fieldName);
|
|
161
|
+
return field ? field.type : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate a primitive value against its expected type
|
|
166
|
+
* @param {any} value - Value to validate
|
|
167
|
+
* @param {object} fieldType - Field type definition
|
|
168
|
+
* @returns {object|null} Validation issue or null if valid
|
|
169
|
+
*/
|
|
170
|
+
function validatePrimitiveValue(value, fieldType) {
|
|
171
|
+
const expectedJsType = PRIMITIVE_TYPE_MAP[fieldType.type];
|
|
172
|
+
const actualType = typeof value;
|
|
173
|
+
|
|
174
|
+
if (!expectedJsType) {
|
|
175
|
+
return null; // Unknown primitive type, skip validation
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Allow number for bigint fields (will be converted)
|
|
179
|
+
if (expectedJsType === 'bigint' && actualType === 'number') {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (actualType !== expectedJsType) {
|
|
184
|
+
return {
|
|
185
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
186
|
+
expected: expectedJsType,
|
|
187
|
+
received: actualType,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validate array constraints
|
|
196
|
+
* @param {any} value - Array value to validate
|
|
197
|
+
* @param {object} fieldType - Field type definition
|
|
198
|
+
* @returns {object|null} Validation issue or null if valid
|
|
199
|
+
*/
|
|
200
|
+
function validateArrayConstraints(value, fieldType) {
|
|
201
|
+
if (!Array.isArray(value) && !isTypedArray(value)) {
|
|
202
|
+
return {
|
|
203
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
204
|
+
expected: 'array',
|
|
205
|
+
received: getValueType(value),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const length = value.length;
|
|
210
|
+
|
|
211
|
+
// Fixed size array
|
|
212
|
+
if (fieldType.isFixedSizeArray && length !== fieldType.arraySize) {
|
|
213
|
+
return {
|
|
214
|
+
problem: ValidationProblem.ARRAY_LENGTH,
|
|
215
|
+
expected: `exactly ${fieldType.arraySize} elements`,
|
|
216
|
+
received: `${length} elements`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Upper bound array
|
|
221
|
+
if (fieldType.isUpperBound && length > fieldType.arraySize) {
|
|
222
|
+
return {
|
|
223
|
+
problem: ValidationProblem.ARRAY_LENGTH,
|
|
224
|
+
expected: `at most ${fieldType.arraySize} elements`,
|
|
225
|
+
received: `${length} elements`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Validate a message object against its schema
|
|
234
|
+
* @param {object} obj - Plain object to validate
|
|
235
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
236
|
+
* @param {object} [options] - Validation options
|
|
237
|
+
* @param {boolean} [options.strict=false] - If true, unknown fields cause validation failure
|
|
238
|
+
* @param {boolean} [options.checkTypes=true] - If true, validate field types
|
|
239
|
+
* @param {boolean} [options.checkRequired=false] - If true, check for missing fields
|
|
240
|
+
* @param {string} [options.path=''] - Current path for nested validation (internal use)
|
|
241
|
+
* @returns {{valid: boolean, issues: Array<object>}} Validation result
|
|
242
|
+
*/
|
|
243
|
+
function validateMessage(obj, typeClass, options = {}) {
|
|
244
|
+
const {
|
|
245
|
+
strict = false,
|
|
246
|
+
checkTypes = true,
|
|
247
|
+
checkRequired = false,
|
|
248
|
+
path = '',
|
|
249
|
+
} = options;
|
|
250
|
+
|
|
251
|
+
const issues = [];
|
|
252
|
+
const resolved = resolveTypeClass(typeClass);
|
|
253
|
+
|
|
254
|
+
if (!resolved) {
|
|
255
|
+
issues.push({
|
|
256
|
+
field: path || '(root)',
|
|
257
|
+
problem: 'INVALID_TYPE_CLASS',
|
|
258
|
+
expected: 'valid message type class',
|
|
259
|
+
received: typeof typeClass,
|
|
260
|
+
});
|
|
261
|
+
return { valid: false, issues };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const schema = getMessageSchema(resolved);
|
|
265
|
+
if (!schema) {
|
|
266
|
+
issues.push({
|
|
267
|
+
field: path || '(root)',
|
|
268
|
+
problem: 'NO_SCHEMA',
|
|
269
|
+
expected: 'message with ROSMessageDef',
|
|
270
|
+
received: 'class without schema',
|
|
271
|
+
});
|
|
272
|
+
return { valid: false, issues };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (obj === null || obj === undefined) {
|
|
276
|
+
issues.push({
|
|
277
|
+
field: path || '(root)',
|
|
278
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
279
|
+
expected: 'object',
|
|
280
|
+
received: String(obj),
|
|
281
|
+
});
|
|
282
|
+
return { valid: false, issues };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const type = typeof obj;
|
|
286
|
+
if (
|
|
287
|
+
type === 'string' ||
|
|
288
|
+
type === 'number' ||
|
|
289
|
+
type === 'boolean' ||
|
|
290
|
+
type === 'bigint'
|
|
291
|
+
) {
|
|
292
|
+
if (schema.fields.length === 1 && schema.fields[0].name === 'data') {
|
|
293
|
+
const fieldType = schema.fields[0].type;
|
|
294
|
+
if (checkTypes && fieldType.isPrimitiveType) {
|
|
295
|
+
const typeIssue = validatePrimitiveValue(obj, fieldType);
|
|
296
|
+
if (typeIssue) {
|
|
297
|
+
issues.push({
|
|
298
|
+
field: path ? `${path}.data` : 'data',
|
|
299
|
+
...typeIssue,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { valid: issues.length === 0, issues };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (type !== 'object') {
|
|
308
|
+
issues.push({
|
|
309
|
+
field: path || '(root)',
|
|
310
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
311
|
+
expected: 'object',
|
|
312
|
+
received: type,
|
|
313
|
+
});
|
|
314
|
+
return { valid: false, issues };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const fieldNames = new Set(schema.fields.map((f) => f.name));
|
|
318
|
+
const objKeys = Object.keys(obj);
|
|
319
|
+
|
|
320
|
+
if (strict) {
|
|
321
|
+
for (const key of objKeys) {
|
|
322
|
+
if (!fieldNames.has(key)) {
|
|
323
|
+
issues.push({
|
|
324
|
+
field: path ? `${path}.${key}` : key,
|
|
325
|
+
problem: ValidationProblem.UNKNOWN_FIELD,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const field of schema.fields) {
|
|
332
|
+
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
333
|
+
const value = obj[field.name];
|
|
334
|
+
const fieldType = field.type;
|
|
335
|
+
|
|
336
|
+
if (field.name.startsWith('_')) continue;
|
|
337
|
+
|
|
338
|
+
if (value === undefined) {
|
|
339
|
+
if (checkRequired) {
|
|
340
|
+
issues.push({
|
|
341
|
+
field: fieldPath,
|
|
342
|
+
problem: ValidationProblem.MISSING_FIELD,
|
|
343
|
+
expected: fieldType.type,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (fieldType.isArray) {
|
|
350
|
+
const arrayIssue = validateArrayConstraints(value, fieldType);
|
|
351
|
+
if (arrayIssue) {
|
|
352
|
+
issues.push({ field: fieldPath, ...arrayIssue });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (checkTypes && Array.isArray(value) && value.length > 0) {
|
|
357
|
+
if (fieldType.isPrimitiveType) {
|
|
358
|
+
for (let i = 0; i < value.length; i++) {
|
|
359
|
+
const elemIssue = validatePrimitiveValue(value[i], fieldType);
|
|
360
|
+
if (elemIssue) {
|
|
361
|
+
issues.push({
|
|
362
|
+
field: `${fieldPath}[${i}]`,
|
|
363
|
+
...elemIssue,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
for (let i = 0; i < value.length; i++) {
|
|
369
|
+
const nestedResult = validateMessage(
|
|
370
|
+
value[i],
|
|
371
|
+
getNestedTypeClass(resolved, field.name),
|
|
372
|
+
{
|
|
373
|
+
strict,
|
|
374
|
+
checkTypes,
|
|
375
|
+
checkRequired,
|
|
376
|
+
path: `${fieldPath}[${i}]`,
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
if (!nestedResult.valid) {
|
|
380
|
+
issues.push(...nestedResult.issues);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} else if (fieldType.isPrimitiveType) {
|
|
386
|
+
if (checkTypes) {
|
|
387
|
+
const typeIssue = validatePrimitiveValue(value, fieldType);
|
|
388
|
+
if (typeIssue) {
|
|
389
|
+
issues.push({ field: fieldPath, ...typeIssue });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
if (value !== null && typeof value === 'object') {
|
|
394
|
+
const nestedTypeClass = getNestedTypeClass(resolved, field.name);
|
|
395
|
+
if (nestedTypeClass) {
|
|
396
|
+
const nestedResult = validateMessage(value, nestedTypeClass, {
|
|
397
|
+
strict,
|
|
398
|
+
checkTypes,
|
|
399
|
+
checkRequired,
|
|
400
|
+
path: fieldPath,
|
|
401
|
+
});
|
|
402
|
+
if (!nestedResult.valid) {
|
|
403
|
+
issues.push(...nestedResult.issues);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else if (checkTypes && value !== null) {
|
|
407
|
+
issues.push({
|
|
408
|
+
field: fieldPath,
|
|
409
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
410
|
+
expected: 'object',
|
|
411
|
+
received: getValueType(value),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { valid: issues.length === 0, issues };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get the type class for a nested field
|
|
422
|
+
* @param {function} parentTypeClass - Parent message type class
|
|
423
|
+
* @param {string} fieldName - Field name
|
|
424
|
+
* @returns {function|null} Nested type class or null
|
|
425
|
+
*/
|
|
426
|
+
function getNestedTypeClass(parentTypeClass, fieldName) {
|
|
427
|
+
try {
|
|
428
|
+
const instance = new parentTypeClass();
|
|
429
|
+
const fieldValue = instance[fieldName];
|
|
430
|
+
|
|
431
|
+
if (
|
|
432
|
+
fieldValue &&
|
|
433
|
+
fieldValue.constructor &&
|
|
434
|
+
fieldValue.constructor.ROSMessageDef
|
|
435
|
+
) {
|
|
436
|
+
return fieldValue.constructor;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (
|
|
440
|
+
fieldValue &&
|
|
441
|
+
fieldValue.classType &&
|
|
442
|
+
fieldValue.classType.elementType
|
|
443
|
+
) {
|
|
444
|
+
return fieldValue.classType.elementType;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
const schema = getMessageSchema(parentTypeClass);
|
|
448
|
+
if (schema) {
|
|
449
|
+
const field = schema.fields.find((f) => f.name === fieldName);
|
|
450
|
+
if (field && !field.type.isPrimitiveType) {
|
|
451
|
+
const typeName = `${field.type.pkgName}/msg/${field.type.type}`;
|
|
452
|
+
return resolveTypeClass(typeName);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Validate a message and throw if invalid
|
|
461
|
+
* @param {object} obj - Plain object to validate
|
|
462
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
463
|
+
* @param {object} [options] - Validation options (same as validateMessage)
|
|
464
|
+
* @throws {MessageValidationError} If validation fails
|
|
465
|
+
* @returns {void}
|
|
466
|
+
*/
|
|
467
|
+
function assertValidMessage(obj, typeClass, options = {}) {
|
|
468
|
+
const result = validateMessage(obj, typeClass, options);
|
|
469
|
+
|
|
470
|
+
if (!result.valid) {
|
|
471
|
+
const resolved = resolveTypeClass(typeClass);
|
|
472
|
+
const messageType = resolved
|
|
473
|
+
? getMessageTypeString(resolved)
|
|
474
|
+
: String(typeClass);
|
|
475
|
+
throw new MessageValidationError(messageType, result.issues);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Create a validator function for a specific message type
|
|
481
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
482
|
+
* @param {object} [defaultOptions] - Default validation options
|
|
483
|
+
* @returns {function} Validator function that takes (obj, options?) and returns validation result
|
|
484
|
+
*/
|
|
485
|
+
function createMessageValidator(typeClass, defaultOptions = {}) {
|
|
486
|
+
const resolved = resolveTypeClass(typeClass);
|
|
487
|
+
if (!resolved) {
|
|
488
|
+
throw new TypeValidationError(
|
|
489
|
+
'typeClass',
|
|
490
|
+
typeClass,
|
|
491
|
+
'valid message type class'
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return function validator(obj, options = {}) {
|
|
496
|
+
return validateMessage(obj, resolved, {
|
|
497
|
+
...defaultOptions,
|
|
498
|
+
...options,
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
module.exports = {
|
|
504
|
+
ValidationProblem,
|
|
505
|
+
getMessageSchema,
|
|
506
|
+
getFieldNames,
|
|
507
|
+
getFieldType,
|
|
508
|
+
validateMessage,
|
|
509
|
+
assertValidMessage,
|
|
510
|
+
createMessageValidator,
|
|
511
|
+
getMessageTypeString,
|
|
512
|
+
};
|
package/lib/node.js
CHANGED
|
@@ -45,6 +45,7 @@ const QoS = require('./qos.js');
|
|
|
45
45
|
const Rates = require('./rate.js');
|
|
46
46
|
const Service = require('./service.js');
|
|
47
47
|
const Subscription = require('./subscription.js');
|
|
48
|
+
const ObservableSubscription = require('./observable_subscription.js');
|
|
48
49
|
const TimeSource = require('./time_source.js');
|
|
49
50
|
const Timer = require('./timer.js');
|
|
50
51
|
const TypeDescriptionService = require('./type_description_service.js');
|
|
@@ -102,7 +103,8 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
102
103
|
namespace,
|
|
103
104
|
context.handle,
|
|
104
105
|
args,
|
|
105
|
-
useGlobalArguments
|
|
106
|
+
useGlobalArguments,
|
|
107
|
+
options.rosoutQos
|
|
106
108
|
);
|
|
107
109
|
Object.defineProperty(this, 'handle', {
|
|
108
110
|
configurable: false,
|
|
@@ -132,6 +134,11 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
132
134
|
this._setParametersCallbacks = [];
|
|
133
135
|
this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
|
|
134
136
|
this._spinning = false;
|
|
137
|
+
this._enableRosout = options.enableRosout;
|
|
138
|
+
|
|
139
|
+
if (this._enableRosout) {
|
|
140
|
+
rclnodejs.initRosoutPublisherForNode(this.handle);
|
|
141
|
+
}
|
|
135
142
|
|
|
136
143
|
this._parameterEventPublisher = this.createPublisher(
|
|
137
144
|
PARAMETER_EVENT_MSG_TYPE,
|
|
@@ -814,6 +821,42 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
814
821
|
return subscription;
|
|
815
822
|
}
|
|
816
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Create a Subscription that returns an RxJS Observable.
|
|
826
|
+
* This allows using reactive programming patterns with ROS 2 messages.
|
|
827
|
+
*
|
|
828
|
+
* @param {function|string|object} typeClass - The ROS message class,
|
|
829
|
+
* OR a string representing the message class, e.g. 'std_msgs/msg/String',
|
|
830
|
+
* OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
|
|
831
|
+
* @param {string} topic - The name of the topic.
|
|
832
|
+
* @param {object} [options] - The options argument used to parameterize the subscription.
|
|
833
|
+
* @param {boolean} [options.enableTypedArray=true] - The topic will use TypedArray if necessary.
|
|
834
|
+
* @param {QoS} [options.qos=QoS.profileDefault] - ROS Middleware "quality of service" settings.
|
|
835
|
+
* @param {boolean} [options.isRaw=false] - The topic is serialized when true.
|
|
836
|
+
* @param {string} [options.serializationMode='default'] - Controls message serialization format.
|
|
837
|
+
* @param {object} [options.contentFilter] - The content-filter (if supported by RMW).
|
|
838
|
+
* @param {SubscriptionEventCallbacks} [eventCallbacks] - The event callbacks for the subscription.
|
|
839
|
+
* @return {ObservableSubscription} - An ObservableSubscription with an RxJS Observable.
|
|
840
|
+
*/
|
|
841
|
+
createObservableSubscription(typeClass, topic, options, eventCallbacks) {
|
|
842
|
+
let observableSubscription = null;
|
|
843
|
+
|
|
844
|
+
const subscription = this.createSubscription(
|
|
845
|
+
typeClass,
|
|
846
|
+
topic,
|
|
847
|
+
options,
|
|
848
|
+
(message) => {
|
|
849
|
+
if (observableSubscription) {
|
|
850
|
+
observableSubscription._emit(message);
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
eventCallbacks
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
observableSubscription = new ObservableSubscription(subscription);
|
|
857
|
+
return observableSubscription;
|
|
858
|
+
}
|
|
859
|
+
|
|
817
860
|
/**
|
|
818
861
|
* Create a Client.
|
|
819
862
|
* @param {function|string|object} typeClass - The ROS message class,
|
|
@@ -1016,6 +1059,11 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
1016
1059
|
|
|
1017
1060
|
this.context.onNodeDestroyed(this);
|
|
1018
1061
|
|
|
1062
|
+
if (this._enableRosout) {
|
|
1063
|
+
rclnodejs.finiRosoutPublisherForNode(this.handle);
|
|
1064
|
+
this._enableRosout = false;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1019
1067
|
this.handle.release();
|
|
1020
1068
|
this._clock = null;
|
|
1021
1069
|
this._timers = [];
|
|
@@ -1349,6 +1397,74 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
1349
1397
|
);
|
|
1350
1398
|
}
|
|
1351
1399
|
|
|
1400
|
+
/**
|
|
1401
|
+
* Return a list of clients on a given service.
|
|
1402
|
+
*
|
|
1403
|
+
* The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
|
|
1404
|
+
* the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
|
|
1405
|
+
*
|
|
1406
|
+
* When the `no_mangle` parameter is `true`, the provided `service` should be a valid
|
|
1407
|
+
* service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
|
|
1408
|
+
* apps). When the `no_mangle` parameter is `false`, the provided `service` should
|
|
1409
|
+
* follow ROS service name conventions.
|
|
1410
|
+
*
|
|
1411
|
+
* `service` may be a relative, private, or fully qualified service name.
|
|
1412
|
+
* A relative or private service will be expanded using this node's namespace and name.
|
|
1413
|
+
* The queried `service` is not remapped.
|
|
1414
|
+
*
|
|
1415
|
+
* @param {string} service - The service on which to find the clients.
|
|
1416
|
+
* @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
|
|
1417
|
+
* name, otherwise it should be a valid ROS service name. Defaults to `false`.
|
|
1418
|
+
* @returns {Array} - list of clients
|
|
1419
|
+
*/
|
|
1420
|
+
getClientsInfoByService(service, noDemangle = false) {
|
|
1421
|
+
if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
|
|
1422
|
+
console.warn(
|
|
1423
|
+
'getClientsInfoByService is not supported by this version of ROS 2'
|
|
1424
|
+
);
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
return rclnodejs.getClientsInfoByService(
|
|
1428
|
+
this.handle,
|
|
1429
|
+
this._getValidatedServiceName(service, noDemangle),
|
|
1430
|
+
noDemangle
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Return a list of servers on a given service.
|
|
1436
|
+
*
|
|
1437
|
+
* The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
|
|
1438
|
+
* the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
|
|
1439
|
+
*
|
|
1440
|
+
* When the `no_mangle` parameter is `true`, the provided `service` should be a valid
|
|
1441
|
+
* service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
|
|
1442
|
+
* apps). When the `no_mangle` parameter is `false`, the provided `service` should
|
|
1443
|
+
* follow ROS service name conventions.
|
|
1444
|
+
*
|
|
1445
|
+
* `service` may be a relative, private, or fully qualified service name.
|
|
1446
|
+
* A relative or private service will be expanded using this node's namespace and name.
|
|
1447
|
+
* The queried `service` is not remapped.
|
|
1448
|
+
*
|
|
1449
|
+
* @param {string} service - The service on which to find the servers.
|
|
1450
|
+
* @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
|
|
1451
|
+
* name, otherwise it should be a valid ROS service name. Defaults to `false`.
|
|
1452
|
+
* @returns {Array} - list of servers
|
|
1453
|
+
*/
|
|
1454
|
+
getServersInfoByService(service, noDemangle = false) {
|
|
1455
|
+
if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
|
|
1456
|
+
console.warn(
|
|
1457
|
+
'getServersInfoByService is not supported by this version of ROS 2'
|
|
1458
|
+
);
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
return rclnodejs.getServersInfoByService(
|
|
1462
|
+
this.handle,
|
|
1463
|
+
this._getValidatedServiceName(service, noDemangle),
|
|
1464
|
+
noDemangle
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1352
1468
|
/**
|
|
1353
1469
|
* Get the list of nodes discovered by the provided node.
|
|
1354
1470
|
* @return {Array<string>} - An array of the names.
|
|
@@ -2142,6 +2258,22 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
2142
2258
|
validateFullTopicName(fqTopicName);
|
|
2143
2259
|
return rclnodejs.remapTopicName(this.handle, fqTopicName);
|
|
2144
2260
|
}
|
|
2261
|
+
|
|
2262
|
+
_getValidatedServiceName(serviceName, noDemangle) {
|
|
2263
|
+
if (typeof serviceName !== 'string') {
|
|
2264
|
+
throw new TypeValidationError('serviceName', serviceName, 'string', {
|
|
2265
|
+
nodeName: this.name(),
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
if (noDemangle) {
|
|
2270
|
+
return serviceName;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
const resolvedServiceName = this.resolveServiceName(serviceName);
|
|
2274
|
+
rclnodejs.validateTopicName(resolvedServiceName);
|
|
2275
|
+
return resolvedServiceName;
|
|
2276
|
+
}
|
|
2145
2277
|
}
|
|
2146
2278
|
|
|
2147
2279
|
/**
|