rclnodejs 1.6.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.
Files changed (84) hide show
  1. package/binding.gyp +2 -0
  2. package/index.js +152 -0
  3. package/lib/action/client.js +109 -10
  4. package/lib/action/deferred.js +8 -2
  5. package/lib/action/server.js +10 -1
  6. package/lib/action/uuid.js +4 -1
  7. package/lib/client.js +218 -4
  8. package/lib/clock.js +182 -1
  9. package/lib/clock_change.js +49 -0
  10. package/lib/clock_event.js +88 -0
  11. package/lib/context.js +12 -2
  12. package/lib/duration.js +37 -12
  13. package/lib/errors.js +621 -0
  14. package/lib/event_handler.js +21 -4
  15. package/lib/interface_loader.js +52 -12
  16. package/lib/lifecycle.js +8 -2
  17. package/lib/logging.js +90 -3
  18. package/lib/message_introspector.js +123 -0
  19. package/lib/message_serialization.js +10 -2
  20. package/lib/message_validation.js +512 -0
  21. package/lib/native_loader.js +9 -4
  22. package/lib/node.js +403 -50
  23. package/lib/node_options.js +40 -1
  24. package/lib/observable_subscription.js +105 -0
  25. package/lib/parameter.js +172 -35
  26. package/lib/parameter_client.js +506 -0
  27. package/lib/parameter_watcher.js +309 -0
  28. package/lib/publisher.js +56 -1
  29. package/lib/qos.js +79 -5
  30. package/lib/rate.js +6 -1
  31. package/lib/serialization.js +7 -2
  32. package/lib/subscription.js +8 -0
  33. package/lib/time.js +136 -21
  34. package/lib/time_source.js +13 -4
  35. package/lib/timer.js +42 -0
  36. package/lib/utils.js +27 -1
  37. package/lib/validator.js +74 -19
  38. package/package.json +4 -2
  39. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  40. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  41. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  42. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  43. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  44. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  45. package/rosidl_gen/message_translator.js +0 -61
  46. package/scripts/config.js +1 -0
  47. package/src/addon.cpp +2 -0
  48. package/src/clock_event.cpp +268 -0
  49. package/src/clock_event.hpp +62 -0
  50. package/src/macros.h +2 -4
  51. package/src/rcl_action_server_bindings.cpp +21 -3
  52. package/src/rcl_bindings.cpp +59 -0
  53. package/src/rcl_context_bindings.cpp +5 -0
  54. package/src/rcl_graph_bindings.cpp +73 -0
  55. package/src/rcl_logging_bindings.cpp +158 -0
  56. package/src/rcl_node_bindings.cpp +14 -2
  57. package/src/rcl_publisher_bindings.cpp +12 -0
  58. package/src/rcl_service_bindings.cpp +7 -6
  59. package/src/rcl_subscription_bindings.cpp +51 -14
  60. package/src/rcl_time_point_bindings.cpp +135 -0
  61. package/src/rcl_timer_bindings.cpp +140 -0
  62. package/src/rcl_utilities.cpp +103 -2
  63. package/src/rcl_utilities.h +7 -1
  64. package/types/action_client.d.ts +27 -2
  65. package/types/base.d.ts +6 -0
  66. package/types/client.d.ts +65 -1
  67. package/types/clock.d.ts +86 -0
  68. package/types/clock_change.d.ts +27 -0
  69. package/types/clock_event.d.ts +51 -0
  70. package/types/errors.d.ts +496 -0
  71. package/types/index.d.ts +10 -0
  72. package/types/logging.d.ts +32 -0
  73. package/types/message_introspector.d.ts +75 -0
  74. package/types/message_validation.d.ts +183 -0
  75. package/types/node.d.ts +107 -0
  76. package/types/node_options.d.ts +13 -0
  77. package/types/observable_subscription.d.ts +39 -0
  78. package/types/parameter_client.d.ts +252 -0
  79. package/types/parameter_watcher.d.ts +104 -0
  80. package/types/publisher.d.ts +28 -1
  81. package/types/qos.d.ts +18 -0
  82. package/types/subscription.d.ts +6 -0
  83. package/types/timer.d.ts +18 -0
  84. package/types/validator.d.ts +86 -0
@@ -0,0 +1,506 @@
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 {
18
+ Parameter,
19
+ ParameterType,
20
+ parameterTypeFromValue,
21
+ } = require('./parameter.js');
22
+ const {
23
+ TypeValidationError,
24
+ ParameterNotFoundError,
25
+ OperationError,
26
+ } = require('./errors.js');
27
+ const validator = require('./validator.js');
28
+ const { normalizeNodeName } = require('./utils.js');
29
+ const debug = require('debug')('rclnodejs:parameter_client');
30
+
31
+ /**
32
+ * @class - Class representing a Parameter Client for accessing parameters on remote nodes
33
+ * @hideconstructor
34
+ */
35
+ class ParameterClient {
36
+ #node;
37
+ #remoteNodeName;
38
+ #timeout;
39
+ #clients;
40
+ #destroyed;
41
+
42
+ /**
43
+ * Create a ParameterClient instance.
44
+ * @param {Node} node - The node to use for creating service clients.
45
+ * @param {string} remoteNodeName - The name of the remote node whose parameters to access.
46
+ * @param {object} [options] - Options for parameter client.
47
+ * @param {number} [options.timeout=5000] - Default timeout in milliseconds for service calls.
48
+ */
49
+ constructor(node, remoteNodeName, options = {}) {
50
+ if (!node) {
51
+ throw new TypeValidationError('node', node, 'Node instance');
52
+ }
53
+ if (!remoteNodeName || typeof remoteNodeName !== 'string') {
54
+ throw new TypeValidationError(
55
+ 'remoteNodeName',
56
+ remoteNodeName,
57
+ 'non-empty string'
58
+ );
59
+ }
60
+
61
+ this.#node = node;
62
+ this.#remoteNodeName = normalizeNodeName(remoteNodeName);
63
+ validator.validateNodeName(this.#remoteNodeName);
64
+
65
+ this.#timeout = options.timeout || 5000;
66
+ this.#clients = new Map();
67
+ this.#destroyed = false;
68
+
69
+ debug(
70
+ `ParameterClient created for remote node: ${this.#remoteNodeName} with timeout: ${this.#timeout}ms`
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Get the remote node name this client is connected to.
76
+ * @return {string} - The remote node name.
77
+ */
78
+ get remoteNodeName() {
79
+ return this.#remoteNodeName;
80
+ }
81
+
82
+ /**
83
+ * Get a single parameter from the remote node.
84
+ * @param {string} name - The name of the parameter to retrieve.
85
+ * @param {object} [options] - Options for the service call.
86
+ * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
87
+ * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
88
+ * @return {Promise<Parameter>} - Promise that resolves with the Parameter object.
89
+ * @throws {Error} If the parameter is not found or service call fails.
90
+ */
91
+ async getParameter(name, options = {}) {
92
+ this.#throwErrorIfClientDestroyed();
93
+
94
+ const parameters = await this.getParameters([name], options);
95
+ if (parameters.length === 0) {
96
+ throw new ParameterNotFoundError(name, this.#remoteNodeName);
97
+ }
98
+
99
+ return parameters[0];
100
+ }
101
+
102
+ /**
103
+ * Get multiple parameters from the remote node.
104
+ * @param {string[]} names - Array of parameter names to retrieve.
105
+ * @param {object} [options] - Options for the service call.
106
+ * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
107
+ * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
108
+ * @return {Promise<Parameter[]>} - Promise that resolves with an array of Parameter objects.
109
+ * @throws {Error} If the service call fails.
110
+ */
111
+ async getParameters(names, options = {}) {
112
+ this.#throwErrorIfClientDestroyed();
113
+
114
+ if (!Array.isArray(names) || names.length === 0) {
115
+ throw new TypeValidationError('names', names, 'non-empty array');
116
+ }
117
+
118
+ const client = this.#getOrCreateClient('GetParameters');
119
+ const request = { names };
120
+
121
+ debug(
122
+ `Getting ${names.length} parameter(s) from node ${this.#remoteNodeName}`
123
+ );
124
+
125
+ const response = await client.sendRequestAsync(request, {
126
+ timeout: options.timeout || this.#timeout,
127
+ signal: options.signal,
128
+ });
129
+
130
+ const parameters = [];
131
+ for (let i = 0; i < names.length; i++) {
132
+ const value = response.values[i];
133
+ if (value.type !== ParameterType.PARAMETER_NOT_SET) {
134
+ parameters.push(
135
+ new Parameter(
136
+ names[i],
137
+ value.type,
138
+ this.#deserializeParameterValue(value)
139
+ )
140
+ );
141
+ }
142
+ }
143
+
144
+ debug(`Retrieved ${parameters.length} parameter(s)`);
145
+ return parameters;
146
+ }
147
+
148
+ /**
149
+ * Set a single parameter on the remote node.
150
+ * @param {string} name - The name of the parameter to set.
151
+ * @param {*} value - The value to set. Type is automatically inferred.
152
+ * @param {object} [options] - Options for the service call.
153
+ * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
154
+ * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
155
+ * @return {Promise<object>} - Promise that resolves with the result {successful: boolean, reason: string}.
156
+ * @throws {Error} If the service call fails.
157
+ */
158
+ async setParameter(name, value, options = {}) {
159
+ this.#throwErrorIfClientDestroyed();
160
+
161
+ const results = await this.setParameters([{ name, value }], options);
162
+ return results[0];
163
+ }
164
+
165
+ /**
166
+ * Set multiple parameters on the remote node.
167
+ * @param {Array<{name: string, value: *}>} parameters - Array of parameter objects with name and value.
168
+ * @param {object} [options] - Options for the service call.
169
+ * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
170
+ * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
171
+ * @return {Promise<Array<{name: string, successful: boolean, reason: string}>>} - Promise that resolves with an array of results.
172
+ * @throws {Error} If the service call fails.
173
+ */
174
+ async setParameters(parameters, options = {}) {
175
+ this.#throwErrorIfClientDestroyed();
176
+
177
+ if (!Array.isArray(parameters) || parameters.length === 0) {
178
+ throw new TypeValidationError(
179
+ 'parameters',
180
+ parameters,
181
+ 'non-empty array'
182
+ );
183
+ }
184
+
185
+ const client = this.#getOrCreateClient('SetParameters');
186
+ const request = {
187
+ parameters: parameters.map((param) => ({
188
+ name: param.name,
189
+ value: this.#serializeParameterValue(param.value),
190
+ })),
191
+ };
192
+
193
+ debug(
194
+ `Setting ${parameters.length} parameter(s) on node ${this.#remoteNodeName}`
195
+ );
196
+
197
+ const response = await client.sendRequestAsync(request, {
198
+ timeout: options.timeout || this.#timeout,
199
+ signal: options.signal,
200
+ });
201
+
202
+ const results = response.results.map((result, index) => ({
203
+ name: parameters[index].name,
204
+ successful: result.successful,
205
+ reason: result.reason || '',
206
+ }));
207
+
208
+ debug(
209
+ `Set ${results.filter((r) => r.successful).length}/${results.length} parameter(s) successfully`
210
+ );
211
+ return results;
212
+ }
213
+
214
+ /**
215
+ * List all parameters available on the remote node.
216
+ * @param {object} [options] - Options for listing parameters.
217
+ * @param {string[]} [options.prefixes] - Optional array of parameter name prefixes to filter by.
218
+ * @param {number} [options.depth=0] - Depth of parameter namespace to list (0 = unlimited).
219
+ * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
220
+ * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
221
+ * @return {Promise<{names: string[], prefixes: string[]}>} - Promise that resolves with parameter names and prefixes.
222
+ * @throws {Error} If the service call fails.
223
+ */
224
+ async listParameters(options = {}) {
225
+ this.#throwErrorIfClientDestroyed();
226
+
227
+ const client = this.#getOrCreateClient('ListParameters');
228
+ const request = {
229
+ prefixes: options.prefixes || [],
230
+ depth: options.depth !== undefined ? BigInt(options.depth) : BigInt(0),
231
+ };
232
+
233
+ debug(`Listing parameters on node ${this.#remoteNodeName}`);
234
+
235
+ const response = await client.sendRequestAsync(request, {
236
+ timeout: options.timeout || this.#timeout,
237
+ signal: options.signal,
238
+ });
239
+
240
+ debug(
241
+ `Listed ${response.result.names.length} parameter(s) and ${response.result.prefixes.length} prefix(es)`
242
+ );
243
+
244
+ return {
245
+ names: response.result.names || [],
246
+ prefixes: response.result.prefixes || [],
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Describe parameters on the remote node.
252
+ * @param {string[]} names - Array of parameter names to describe.
253
+ * @param {object} [options] - Options for the service call.
254
+ * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
255
+ * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
256
+ * @return {Promise<Array<object>>} - Promise that resolves with an array of parameter descriptors.
257
+ * @throws {Error} If the service call fails.
258
+ */
259
+ async describeParameters(names, options = {}) {
260
+ this.#throwErrorIfClientDestroyed();
261
+
262
+ if (!Array.isArray(names) || names.length === 0) {
263
+ throw new TypeValidationError('names', names, 'non-empty array');
264
+ }
265
+
266
+ const client = this.#getOrCreateClient('DescribeParameters');
267
+ const request = { names };
268
+
269
+ debug(
270
+ `Describing ${names.length} parameter(s) on node ${this.#remoteNodeName}`
271
+ );
272
+
273
+ const response = await client.sendRequestAsync(request, {
274
+ timeout: options.timeout || this.#timeout,
275
+ signal: options.signal,
276
+ });
277
+
278
+ debug(`Described ${response.descriptors.length} parameter(s)`);
279
+ return response.descriptors || [];
280
+ }
281
+
282
+ /**
283
+ * Get the types of parameters on the remote node.
284
+ * @param {string[]} names - Array of parameter names.
285
+ * @param {object} [options] - Options for the service call.
286
+ * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
287
+ * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
288
+ * @return {Promise<Array<number>>} - Promise that resolves with an array of parameter types.
289
+ * @throws {Error} If the service call fails.
290
+ */
291
+ async getParameterTypes(names, options = {}) {
292
+ this.#throwErrorIfClientDestroyed();
293
+
294
+ if (!Array.isArray(names) || names.length === 0) {
295
+ throw new TypeValidationError('names', names, 'non-empty array');
296
+ }
297
+
298
+ const client = this.#getOrCreateClient('GetParameterTypes');
299
+ const request = { names };
300
+
301
+ debug(
302
+ `Getting types for ${names.length} parameter(s) on node ${this.#remoteNodeName}`
303
+ );
304
+
305
+ const response = await client.sendRequestAsync(request, {
306
+ timeout: options.timeout || this.#timeout,
307
+ signal: options.signal,
308
+ });
309
+
310
+ return response.types || [];
311
+ }
312
+
313
+ /**
314
+ * Wait for the parameter services to be available on the remote node.
315
+ * @param {number} [timeout] - Optional timeout in milliseconds.
316
+ * @return {Promise<boolean>} - Promise that resolves to true if services are available.
317
+ */
318
+ async waitForService(timeout) {
319
+ this.#throwErrorIfClientDestroyed();
320
+
321
+ const client = this.#getOrCreateClient('GetParameters');
322
+ return await client.waitForService(timeout);
323
+ }
324
+
325
+ /**
326
+ * Check if the parameter client has been destroyed.
327
+ * @return {boolean} - True if destroyed, false otherwise.
328
+ */
329
+ isDestroyed() {
330
+ return this.#destroyed;
331
+ }
332
+
333
+ /**
334
+ * Destroy the parameter client and clean up all service clients.
335
+ * @return {undefined}
336
+ */
337
+ destroy() {
338
+ if (this.#destroyed) {
339
+ return;
340
+ }
341
+
342
+ debug(`Destroying ParameterClient for node ${this.#remoteNodeName}`);
343
+
344
+ for (const [serviceType, client] of this.#clients.entries()) {
345
+ try {
346
+ this.#node.destroyClient(client);
347
+ debug(`Destroyed client for service type: ${serviceType}`);
348
+ } catch (error) {
349
+ debug(
350
+ `Error destroying client for service type ${serviceType}:`,
351
+ error
352
+ );
353
+ }
354
+ }
355
+
356
+ this.#clients.clear();
357
+ this.#destroyed = true;
358
+
359
+ debug('ParameterClient destroyed');
360
+ }
361
+
362
+ /**
363
+ * Get or create a service client for the specified service type.
364
+ * @private
365
+ * @param {string} serviceType - The service type (e.g., 'GetParameters', 'SetParameters').
366
+ * @return {Client} - The service client.
367
+ */
368
+ #getOrCreateClient(serviceType) {
369
+ if (this.#clients.has(serviceType)) {
370
+ return this.#clients.get(serviceType);
371
+ }
372
+
373
+ const serviceName = `/${this.#remoteNodeName}/${this.#toSnakeCase(serviceType)}`;
374
+ const serviceInterface = `rcl_interfaces/srv/${serviceType}`;
375
+
376
+ debug(`Creating client for service: ${serviceName}`);
377
+
378
+ const client = this.#node.createClient(serviceInterface, serviceName);
379
+ this.#clients.set(serviceType, client);
380
+
381
+ return client;
382
+ }
383
+
384
+ /**
385
+ * Serialize a JavaScript value to a ParameterValue message.
386
+ * @private
387
+ * @param {*} value - The value to serialize.
388
+ * @return {object} - The ParameterValue message.
389
+ */
390
+ #serializeParameterValue(value) {
391
+ const type = parameterTypeFromValue(value);
392
+
393
+ const paramValue = {
394
+ type,
395
+ bool_value: false,
396
+ integer_value: BigInt(0),
397
+ double_value: 0.0,
398
+ string_value: '',
399
+ byte_array_value: [],
400
+ bool_array_value: [],
401
+ integer_array_value: [],
402
+ double_array_value: [],
403
+ string_array_value: [],
404
+ };
405
+
406
+ switch (type) {
407
+ case ParameterType.PARAMETER_BOOL:
408
+ paramValue.bool_value = value;
409
+ break;
410
+ case ParameterType.PARAMETER_INTEGER:
411
+ paramValue.integer_value =
412
+ typeof value === 'bigint' ? value : BigInt(value);
413
+ break;
414
+ case ParameterType.PARAMETER_DOUBLE:
415
+ paramValue.double_value = value;
416
+ break;
417
+ case ParameterType.PARAMETER_STRING:
418
+ paramValue.string_value = value;
419
+ break;
420
+ case ParameterType.PARAMETER_BOOL_ARRAY:
421
+ paramValue.bool_array_value = Array.from(value);
422
+ break;
423
+ case ParameterType.PARAMETER_INTEGER_ARRAY:
424
+ paramValue.integer_array_value = Array.from(value).map((v) =>
425
+ typeof v === 'bigint' ? v : BigInt(v)
426
+ );
427
+ break;
428
+ case ParameterType.PARAMETER_DOUBLE_ARRAY:
429
+ paramValue.double_array_value = Array.from(value);
430
+ break;
431
+ case ParameterType.PARAMETER_STRING_ARRAY:
432
+ paramValue.string_array_value = Array.from(value);
433
+ break;
434
+ case ParameterType.PARAMETER_BYTE_ARRAY:
435
+ paramValue.byte_array_value = Array.from(value).map((v) =>
436
+ Math.trunc(v)
437
+ );
438
+ break;
439
+ }
440
+
441
+ return paramValue;
442
+ }
443
+
444
+ /**
445
+ * Deserialize a ParameterValue message to a JavaScript value.
446
+ * @private
447
+ * @param {object} paramValue - The ParameterValue message.
448
+ * @return {*} - The deserialized value.
449
+ */
450
+ #deserializeParameterValue(paramValue) {
451
+ switch (paramValue.type) {
452
+ case ParameterType.PARAMETER_BOOL:
453
+ return paramValue.bool_value;
454
+ case ParameterType.PARAMETER_INTEGER:
455
+ return paramValue.integer_value;
456
+ case ParameterType.PARAMETER_DOUBLE:
457
+ return paramValue.double_value;
458
+ case ParameterType.PARAMETER_STRING:
459
+ return paramValue.string_value;
460
+ case ParameterType.PARAMETER_BYTE_ARRAY:
461
+ return Array.from(paramValue.byte_array_value || []);
462
+ case ParameterType.PARAMETER_BOOL_ARRAY:
463
+ return Array.from(paramValue.bool_array_value || []);
464
+ case ParameterType.PARAMETER_INTEGER_ARRAY:
465
+ return Array.from(paramValue.integer_array_value || []).map((v) =>
466
+ typeof v === 'bigint' ? v : BigInt(v)
467
+ );
468
+ case ParameterType.PARAMETER_DOUBLE_ARRAY:
469
+ return Array.from(paramValue.double_array_value || []);
470
+ case ParameterType.PARAMETER_STRING_ARRAY:
471
+ return Array.from(paramValue.string_array_value || []);
472
+ case ParameterType.PARAMETER_NOT_SET:
473
+ default:
474
+ return null;
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Convert a service type name from PascalCase to snake_case.
480
+ * @private
481
+ * @param {string} name - The name to convert.
482
+ * @return {string} - The snake_case name.
483
+ */
484
+ #toSnakeCase(name) {
485
+ return name.replace(/[A-Z]/g, (letter, index) => {
486
+ return index === 0 ? letter.toLowerCase() : '_' + letter.toLowerCase();
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Throws an error if the client has been destroyed.
492
+ * @private
493
+ * @throws {Error} If the client has been destroyed.
494
+ */
495
+ #throwErrorIfClientDestroyed() {
496
+ if (this.#destroyed) {
497
+ throw new OperationError('ParameterClient has been destroyed', {
498
+ code: 'CLIENT_DESTROYED',
499
+ entityType: 'parameter_client',
500
+ entityName: this.#remoteNodeName,
501
+ });
502
+ }
503
+ }
504
+ }
505
+
506
+ module.exports = ParameterClient;