rclnodejs 1.8.3 → 1.9.0-alpha.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 (85) hide show
  1. package/README.md +46 -37
  2. package/index.js +29 -0
  3. package/lib/action/client.js +61 -3
  4. package/lib/message_info.js +94 -0
  5. package/lib/node.js +53 -3
  6. package/lib/parameter_event_handler.js +468 -0
  7. package/lib/parameter_watcher.js +12 -12
  8. package/lib/subscription.js +38 -5
  9. package/lib/timer.js +2 -1
  10. package/lib/wait_for_message.js +111 -0
  11. package/package.json +6 -3
  12. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  13. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  14. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  15. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  16. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  17. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  18. package/scripts/run_asan_test.sh +118 -0
  19. package/src/executor.cpp +36 -2
  20. package/src/executor.h +11 -0
  21. package/src/rcl_action_client_bindings.cpp +70 -1
  22. package/src/rcl_context_bindings.cpp +3 -3
  23. package/src/rcl_graph_bindings.cpp +2 -2
  24. package/src/rcl_subscription_bindings.cpp +70 -2
  25. package/src/rcl_utilities.cpp +2 -2
  26. package/tools/jsdoc/Makefile +5 -0
  27. package/tools/jsdoc/README.md +96 -0
  28. package/tools/jsdoc/build-index.js +610 -0
  29. package/tools/jsdoc/publish.js +854 -0
  30. package/tools/jsdoc/regenerate-published-docs.js +605 -0
  31. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  32. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  33. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  34. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  35. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  36. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  37. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  38. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  39. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  40. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  41. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  42. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  43. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  44. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  45. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  46. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  47. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  48. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  49. package/tools/jsdoc/static/scripts/linenumber.js +25 -0
  50. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  51. package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
  52. package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
  53. package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
  54. package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
  55. package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
  56. package/tools/jsdoc/tmpl/augments.tmpl +10 -0
  57. package/tools/jsdoc/tmpl/container.tmpl +193 -0
  58. package/tools/jsdoc/tmpl/details.tmpl +143 -0
  59. package/tools/jsdoc/tmpl/example.tmpl +2 -0
  60. package/tools/jsdoc/tmpl/examples.tmpl +13 -0
  61. package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
  62. package/tools/jsdoc/tmpl/layout.tmpl +83 -0
  63. package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
  64. package/tools/jsdoc/tmpl/members.tmpl +43 -0
  65. package/tools/jsdoc/tmpl/method.tmpl +124 -0
  66. package/tools/jsdoc/tmpl/params.tmpl +133 -0
  67. package/tools/jsdoc/tmpl/properties.tmpl +110 -0
  68. package/tools/jsdoc/tmpl/returns.tmpl +12 -0
  69. package/tools/jsdoc/tmpl/source.tmpl +8 -0
  70. package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
  71. package/tools/jsdoc/tmpl/type.tmpl +7 -0
  72. package/types/action_client.d.ts +8 -0
  73. package/types/index.d.ts +34 -0
  74. package/types/message_info.d.ts +72 -0
  75. package/types/node.d.ts +21 -0
  76. package/types/parameter_event_handler.d.ts +139 -0
  77. package/types/subscription.d.ts +14 -2
  78. package/test_data_integrity.js +0 -108
  79. package/test_repro_exact.js +0 -57
  80. package/test_repro_hz.js +0 -86
  81. package/test_repro_pub.js +0 -36
  82. package/test_repro_stress.js +0 -83
  83. package/test_repro_sub.js +0 -64
  84. package/test_xproc_data.js +0 -64
  85. package/types/interfaces.d.ts +0 -8895
@@ -0,0 +1,468 @@
1
+ // Copyright (c) 2026, The Robot Web Tools Contributors
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 { TypeValidationError, OperationError } = require('./errors');
18
+ const { normalizeNodeName } = require('./utils');
19
+ const debug = require('debug')('rclnodejs:parameter_event_handler');
20
+
21
+ const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
22
+ const PARAMETER_EVENT_TOPIC = '/parameter_events';
23
+
24
+ /**
25
+ * @class ParameterCallbackHandle
26
+ * Opaque handle returned when adding a parameter callback.
27
+ * Used to remove the callback later.
28
+ */
29
+ class ParameterCallbackHandle {
30
+ /**
31
+ * @param {string} parameterName - The parameter name
32
+ * @param {string} nodeName - The fully qualified node name
33
+ * @param {Function} callback - The callback function
34
+ * @hideconstructor
35
+ */
36
+ constructor(parameterName, nodeName, callback) {
37
+ this.parameterName = parameterName;
38
+ this.nodeName = nodeName;
39
+ this.callback = callback;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @class ParameterEventCallbackHandle
45
+ * Opaque handle returned when adding a parameter event callback.
46
+ * Used to remove the callback later.
47
+ */
48
+ class ParameterEventCallbackHandle {
49
+ /**
50
+ * @param {Function} callback - The callback function
51
+ * @hideconstructor
52
+ */
53
+ constructor(callback) {
54
+ this.callback = callback;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @class ParameterEventHandler
60
+ *
61
+ * Monitors and responds to parameter changes on any node in the ROS 2 graph
62
+ * by subscribing to the `/parameter_events` topic.
63
+ *
64
+ * Unlike {@link ParameterWatcher}, which is tied to a single remote node and
65
+ * requires waiting for that node's parameter services, ParameterEventHandler
66
+ * responds to parameter events from any node without needing service availability.
67
+ *
68
+ * Two types of callbacks are supported:
69
+ * - **Parameter callbacks**: fired when a specific parameter on a specific node
70
+ * is added or changed (new_parameters + changed_parameters).
71
+ * Note: deleted parameters are not dispatched to parameter callbacks;
72
+ * use event callbacks to observe deletions.
73
+ * - **Event callbacks**: fired for every ParameterEvent message received,
74
+ * including deletions.
75
+ *
76
+ * @example
77
+ * const handler = node.createParameterEventHandler();
78
+ *
79
+ * // Watch a specific parameter on a specific node
80
+ * const handle = handler.addParameterCallback(
81
+ * 'my_param',
82
+ * '/my_node',
83
+ * (parameter) => {
84
+ * console.log(`Parameter changed: ${parameter.name} = ${parameter.value}`);
85
+ * }
86
+ * );
87
+ *
88
+ * // Watch all parameter events
89
+ * const eventHandle = handler.addParameterEventCallback((event) => {
90
+ * console.log(`Event from node: ${event.node}`);
91
+ * });
92
+ *
93
+ * // Remove callbacks when done
94
+ * handler.removeParameterCallback(handle);
95
+ * handler.removeParameterEventCallback(eventHandle);
96
+ *
97
+ * // Destroy when no longer needed
98
+ * handler.destroy();
99
+ */
100
+ class ParameterEventHandler {
101
+ #node;
102
+ #subscription;
103
+ #parameterCallbacks; // Map<string, ParameterCallbackHandle[]> keyed by "paramName\0nodeName"
104
+ #eventCallbacks; // ParameterEventCallbackHandle[]
105
+ #destroyed;
106
+
107
+ /**
108
+ * Create a ParameterEventHandler.
109
+ *
110
+ * @param {object} node - The rclnodejs Node used to create the subscription
111
+ * @param {object} [options] - Options
112
+ * @param {object} [options.qos] - QoS profile for the parameter_events subscription
113
+ */
114
+ constructor(node, options = {}) {
115
+ if (!node || typeof node.createSubscription !== 'function') {
116
+ throw new TypeValidationError('node', node, 'Node instance', {
117
+ entityType: 'parameter event handler',
118
+ });
119
+ }
120
+
121
+ if (
122
+ options !== undefined &&
123
+ options !== null &&
124
+ typeof options !== 'object'
125
+ ) {
126
+ throw new TypeValidationError('options', options, 'object or undefined', {
127
+ entityType: 'parameter event handler',
128
+ });
129
+ }
130
+
131
+ const opts = options || {};
132
+
133
+ this.#node = node;
134
+ this.#parameterCallbacks = new Map();
135
+ this.#eventCallbacks = [];
136
+ this.#destroyed = false;
137
+
138
+ const subscriptionOptions = opts.qos ? { qos: opts.qos } : undefined;
139
+
140
+ this.#subscription = node.createSubscription(
141
+ PARAMETER_EVENT_MSG_TYPE,
142
+ PARAMETER_EVENT_TOPIC,
143
+ subscriptionOptions,
144
+ (event) => this.#handleEvent(event)
145
+ );
146
+
147
+ debug('Created ParameterEventHandler on node=%s', node.name());
148
+ }
149
+
150
+ /**
151
+ * Add a callback for a specific parameter on a specific node.
152
+ *
153
+ * The callback is invoked whenever the named parameter is added or changed
154
+ * on the specified node. The callback receives the parameter message object
155
+ * (rcl_interfaces/msg/Parameter) with `name` and `value` fields.
156
+ *
157
+ * @param {string} parameterName - Name of the parameter to monitor
158
+ * @param {string} nodeName - Fully qualified name of the node (e.g., '/my_node')
159
+ * @param {Function} callback - Called with (parameter) when the parameter changes
160
+ * @returns {ParameterCallbackHandle} Handle for removing this callback later
161
+ * @throws {Error} If the handler has been destroyed
162
+ * @throws {TypeError} If arguments are invalid
163
+ */
164
+ addParameterCallback(parameterName, nodeName, callback) {
165
+ this.#checkNotDestroyed();
166
+
167
+ if (typeof parameterName !== 'string' || parameterName.trim() === '') {
168
+ throw new TypeValidationError(
169
+ 'parameterName',
170
+ parameterName,
171
+ 'non-empty string',
172
+ { entityType: 'parameter event handler' }
173
+ );
174
+ }
175
+
176
+ if (typeof nodeName !== 'string' || nodeName.trim() === '') {
177
+ throw new TypeValidationError('nodeName', nodeName, 'non-empty string', {
178
+ entityType: 'parameter event handler',
179
+ });
180
+ }
181
+
182
+ if (typeof callback !== 'function') {
183
+ throw new TypeValidationError('callback', callback, 'function', {
184
+ entityType: 'parameter event handler',
185
+ });
186
+ }
187
+
188
+ const resolvedNodeName = normalizeNodeName(nodeName);
189
+ const resolvedParamName = parameterName.trim();
190
+ const handle = new ParameterCallbackHandle(
191
+ resolvedParamName,
192
+ resolvedNodeName,
193
+ callback
194
+ );
195
+ const key = this.#makeKey(resolvedParamName, resolvedNodeName);
196
+
197
+ if (!this.#parameterCallbacks.has(key)) {
198
+ this.#parameterCallbacks.set(key, []);
199
+ }
200
+
201
+ // Insert at front (FILO order, matching rclpy behavior)
202
+ this.#parameterCallbacks.get(key).unshift(handle);
203
+
204
+ debug(
205
+ 'Added parameter callback: param=%s node=%s',
206
+ resolvedParamName,
207
+ resolvedNodeName
208
+ );
209
+
210
+ return handle;
211
+ }
212
+
213
+ /**
214
+ * Remove a previously added parameter callback.
215
+ *
216
+ * @param {ParameterCallbackHandle} handle - The handle returned by addParameterCallback
217
+ * @throws {Error} If the handle is not found or handler is destroyed
218
+ */
219
+ removeParameterCallback(handle) {
220
+ this.#checkNotDestroyed();
221
+
222
+ if (!(handle instanceof ParameterCallbackHandle)) {
223
+ throw new TypeValidationError(
224
+ 'handle',
225
+ handle,
226
+ 'ParameterCallbackHandle',
227
+ { entityType: 'parameter event handler' }
228
+ );
229
+ }
230
+
231
+ const key = this.#makeKey(handle.parameterName, handle.nodeName);
232
+ const callbacks = this.#parameterCallbacks.get(key);
233
+
234
+ if (!callbacks) {
235
+ throw new OperationError(
236
+ `No callbacks registered for parameter '${handle.parameterName}' on node '${handle.nodeName}'`,
237
+ { entityType: 'parameter event handler' }
238
+ );
239
+ }
240
+
241
+ const index = callbacks.indexOf(handle);
242
+ if (index === -1) {
243
+ throw new OperationError("Callback doesn't exist", {
244
+ entityType: 'parameter event handler',
245
+ });
246
+ }
247
+
248
+ callbacks.splice(index, 1);
249
+
250
+ if (callbacks.length === 0) {
251
+ this.#parameterCallbacks.delete(key);
252
+ }
253
+
254
+ debug(
255
+ 'Removed parameter callback: param=%s node=%s',
256
+ handle.parameterName,
257
+ handle.nodeName
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Add a callback that is invoked for every parameter event.
263
+ *
264
+ * The callback receives the full ParameterEvent message
265
+ * (rcl_interfaces/msg/ParameterEvent) with `node`, `new_parameters`,
266
+ * `changed_parameters`, and `deleted_parameters` fields.
267
+ *
268
+ * @param {Function} callback - Called with (event) for every ParameterEvent
269
+ * @returns {ParameterEventCallbackHandle} Handle for removing this callback later
270
+ * @throws {Error} If the handler has been destroyed
271
+ * @throws {TypeError} If callback is not a function
272
+ */
273
+ addParameterEventCallback(callback) {
274
+ this.#checkNotDestroyed();
275
+
276
+ if (typeof callback !== 'function') {
277
+ throw new TypeValidationError('callback', callback, 'function', {
278
+ entityType: 'parameter event handler',
279
+ });
280
+ }
281
+
282
+ const handle = new ParameterEventCallbackHandle(callback);
283
+
284
+ // Insert at front (FILO order)
285
+ this.#eventCallbacks.unshift(handle);
286
+
287
+ debug('Added parameter event callback');
288
+
289
+ return handle;
290
+ }
291
+
292
+ /**
293
+ * Remove a previously added parameter event callback.
294
+ *
295
+ * @param {ParameterEventCallbackHandle} handle - The handle returned by addParameterEventCallback
296
+ * @throws {Error} If the handle is not found or handler is destroyed
297
+ */
298
+ removeParameterEventCallback(handle) {
299
+ this.#checkNotDestroyed();
300
+
301
+ if (!(handle instanceof ParameterEventCallbackHandle)) {
302
+ throw new TypeValidationError(
303
+ 'handle',
304
+ handle,
305
+ 'ParameterEventCallbackHandle',
306
+ { entityType: 'parameter event handler' }
307
+ );
308
+ }
309
+
310
+ const index = this.#eventCallbacks.indexOf(handle);
311
+ if (index === -1) {
312
+ throw new OperationError("Callback doesn't exist", {
313
+ entityType: 'parameter event handler',
314
+ });
315
+ }
316
+
317
+ this.#eventCallbacks.splice(index, 1);
318
+
319
+ debug('Removed parameter event callback');
320
+ }
321
+
322
+ /**
323
+ * Check if the handler has been destroyed.
324
+ *
325
+ * @returns {boolean} True if destroyed
326
+ */
327
+ isDestroyed() {
328
+ return this.#destroyed;
329
+ }
330
+
331
+ /**
332
+ * Destroy the handler and clean up resources.
333
+ * Removes the subscription and clears all callbacks.
334
+ */
335
+ destroy() {
336
+ if (this.#destroyed) {
337
+ return;
338
+ }
339
+
340
+ debug('Destroying ParameterEventHandler');
341
+
342
+ if (this.#subscription) {
343
+ try {
344
+ this.#node.destroySubscription(this.#subscription);
345
+ } catch (error) {
346
+ debug('Error destroying subscription: %s', error.message);
347
+ }
348
+ this.#subscription = null;
349
+ }
350
+
351
+ this.#parameterCallbacks.clear();
352
+ this.#eventCallbacks.length = 0;
353
+ this.#destroyed = true;
354
+ }
355
+
356
+ /**
357
+ * Get a specific parameter from a ParameterEvent message.
358
+ *
359
+ * @param {object} event - A ParameterEvent message
360
+ * @param {string} parameterName - The parameter name to look for
361
+ * @param {string} nodeName - The node name to match
362
+ * @returns {object|null} The matching parameter message, or null
363
+ * @static
364
+ */
365
+ static getParameterFromEvent(event, parameterName, nodeName) {
366
+ const resolvedNodeName = normalizeNodeName(nodeName);
367
+ const resolvedParamName = (parameterName || '').trim();
368
+
369
+ if (normalizeNodeName(event.node) !== resolvedNodeName) {
370
+ return null;
371
+ }
372
+
373
+ const allParams = [
374
+ ...(event.new_parameters || []),
375
+ ...(event.changed_parameters || []),
376
+ ];
377
+
378
+ for (const param of allParams) {
379
+ if (param.name === resolvedParamName) {
380
+ return param;
381
+ }
382
+ }
383
+
384
+ return null;
385
+ }
386
+
387
+ /**
388
+ * Get all parameters from a ParameterEvent message (new + changed).
389
+ *
390
+ * @param {object} event - A ParameterEvent message
391
+ * @returns {object[]} Array of parameter messages
392
+ * @static
393
+ */
394
+ static getParametersFromEvent(event) {
395
+ return [
396
+ ...(event.new_parameters || []),
397
+ ...(event.changed_parameters || []),
398
+ ];
399
+ }
400
+
401
+ /**
402
+ * Handle incoming parameter event.
403
+ * @private
404
+ */
405
+ #handleEvent(event) {
406
+ const eventNodeName = normalizeNodeName(event.node);
407
+
408
+ // Dispatch parameter-specific callbacks by iterating event params
409
+ // and doing direct Map lookups (O(event_params) instead of O(registered_callbacks))
410
+ const allParams = [
411
+ ...(event.new_parameters || []),
412
+ ...(event.changed_parameters || []),
413
+ ];
414
+
415
+ for (const parameter of allParams) {
416
+ const key = this.#makeKey(parameter.name, eventNodeName);
417
+ const callbacks = this.#parameterCallbacks.get(key);
418
+
419
+ if (callbacks) {
420
+ for (const handle of callbacks.slice()) {
421
+ try {
422
+ handle.callback(parameter);
423
+ } catch (err) {
424
+ debug(
425
+ 'Error in parameter callback for %s on %s: %s',
426
+ parameter.name,
427
+ eventNodeName,
428
+ err.message
429
+ );
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ // Dispatch event-level callbacks
436
+ for (const handle of this.#eventCallbacks.slice()) {
437
+ try {
438
+ handle.callback(event);
439
+ } catch (err) {
440
+ debug('Error in parameter event callback: %s', err.message);
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Create a map key from parameter name and node name.
447
+ * @private
448
+ */
449
+ #makeKey(paramName, nodeName) {
450
+ return `${paramName}\0${nodeName}`;
451
+ }
452
+
453
+ /**
454
+ * Check if the handler has been destroyed and throw if so.
455
+ * @private
456
+ */
457
+ #checkNotDestroyed() {
458
+ if (this.#destroyed) {
459
+ throw new OperationError('ParameterEventHandler has been destroyed', {
460
+ entityType: 'parameter event handler',
461
+ });
462
+ }
463
+ }
464
+ }
465
+
466
+ module.exports = ParameterEventHandler;
467
+ module.exports.ParameterCallbackHandle = ParameterCallbackHandle;
468
+ module.exports.ParameterEventCallbackHandle = ParameterEventCallbackHandle;
@@ -17,6 +17,7 @@
17
17
  const EventEmitter = require('events');
18
18
  const { TypeValidationError, OperationError } = require('./errors');
19
19
  const { normalizeNodeName } = require('./utils');
20
+ const ParameterEventHandler = require('./parameter_event_handler.js');
20
21
  const debug = require('debug')('rclnodejs:parameter_watcher');
21
22
 
22
23
  /**
@@ -30,7 +31,7 @@ const debug = require('debug')('rclnodejs:parameter_watcher');
30
31
  class ParameterWatcher extends EventEmitter {
31
32
  #node;
32
33
  #paramClient;
33
- #subscription;
34
+ #eventHandler;
34
35
  #watchedParams;
35
36
  #remoteNodeName;
36
37
  #destroyed;
@@ -82,7 +83,7 @@ class ParameterWatcher extends EventEmitter {
82
83
  this.#paramClient = node.createParameterClient(remoteNodeName, options);
83
84
  // Cache the remote node name for error messages (in case paramClient is destroyed)
84
85
  this.#remoteNodeName = this.#paramClient.remoteNodeName;
85
- this.#subscription = null;
86
+ this.#eventHandler = null;
86
87
  this.#destroyed = false;
87
88
 
88
89
  debug(
@@ -133,14 +134,13 @@ class ParameterWatcher extends EventEmitter {
133
134
  return false;
134
135
  }
135
136
 
136
- if (!this.#subscription) {
137
- this.#subscription = this.#node.createSubscription(
138
- 'rcl_interfaces/msg/ParameterEvent',
139
- '/parameter_events',
140
- (event) => this.#handleParameterEvent(event)
137
+ if (!this.#eventHandler) {
138
+ this.#eventHandler = new ParameterEventHandler(this.#node);
139
+ this.#eventHandler.addParameterEventCallback((event) =>
140
+ this.#handleParameterEvent(event)
141
141
  );
142
142
 
143
- debug('Subscribed to /parameter_events');
143
+ debug('Subscribed to /parameter_events via ParameterEventHandler');
144
144
  }
145
145
 
146
146
  return true;
@@ -227,13 +227,13 @@ class ParameterWatcher extends EventEmitter {
227
227
 
228
228
  debug('Destroying ParameterWatcher for node=%s', this.remoteNodeName);
229
229
 
230
- if (this.#subscription) {
230
+ if (this.#eventHandler) {
231
231
  try {
232
- this.#node.destroySubscription(this.#subscription);
232
+ this.#eventHandler.destroy();
233
233
  } catch (error) {
234
- debug('Error destroying subscription: %s', error.message);
234
+ debug('Error destroying event handler: %s', error.message);
235
235
  }
236
- this.#subscription = null;
236
+ this.#eventHandler = null;
237
237
  }
238
238
 
239
239
  if (this.#paramClient) {
@@ -16,14 +16,16 @@
16
16
 
17
17
  const rclnodejs = require('./native_loader.js');
18
18
  const Entity = require('./entity.js');
19
+ const DistroUtils = require('./distro.js');
19
20
  const { applySerializationMode } = require('./message_serialization.js');
20
21
  const debug = require('debug')('rclnodejs:subscription');
21
22
 
22
23
  /**
23
24
  * @class - Class representing a ROS 2 Subscription
24
- * @hideconstructor
25
25
  * Includes support for content-filtering topics beginning with the
26
- * ROS Humble release. To learn more about content-filtering
26
+ * ROS Humble release. To learn more about content-filtering topics,
27
+ * see the references below.
28
+ * @hideconstructor
27
29
  * @see {@link Node#options}
28
30
  * @see {@link Node#createSubscription}
29
31
  * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
@@ -45,6 +47,7 @@ class Subscription extends Entity {
45
47
  this._isRaw = options.isRaw || false;
46
48
  this._serializationMode = options.serializationMode || 'default';
47
49
  this._node = node;
50
+ this._wantsMessageInfo = callback.length >= 2;
48
51
 
49
52
  if (node && eventCallbacks) {
50
53
  this._events = eventCallbacks.createEventHandlers(this.handle);
@@ -52,10 +55,24 @@ class Subscription extends Entity {
52
55
  }
53
56
  }
54
57
 
55
- processResponse(msg) {
58
+ /**
59
+ * Whether this subscription's callback wants MessageInfo as a second argument.
60
+ * Determined by callback.length >= 2.
61
+ * @type {boolean}
62
+ * @readonly
63
+ */
64
+ get wantsMessageInfo() {
65
+ return this._wantsMessageInfo;
66
+ }
67
+
68
+ processResponse(msg, messageInfo) {
56
69
  debug(`Message of topic ${this._topic} received.`);
57
70
  if (this._isRaw) {
58
- this._callback(msg);
71
+ if (this._wantsMessageInfo && messageInfo) {
72
+ this._callback(msg, messageInfo);
73
+ } else {
74
+ this._callback(msg);
75
+ }
59
76
  } else {
60
77
  let message = msg.toPlainObject(this.typedArrayEnabled);
61
78
 
@@ -63,7 +80,11 @@ class Subscription extends Entity {
63
80
  message = applySerializationMode(message, this._serializationMode);
64
81
  }
65
82
 
66
- this._callback(message);
83
+ if (this._wantsMessageInfo && messageInfo) {
84
+ this._callback(message, messageInfo);
85
+ } else {
86
+ this._callback(message);
87
+ }
67
88
  }
68
89
  }
69
90
 
@@ -124,6 +145,18 @@ class Subscription extends Entity {
124
145
  return this._serializationMode;
125
146
  }
126
147
 
148
+ /**
149
+ * Check if content filtering is supported for this subscription.
150
+ * Requires ROS 2 Rolling or later.
151
+ * @returns {boolean} True if the subscription instance supports content filtering; otherwise false.
152
+ */
153
+ isContentFilterSupported() {
154
+ if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
155
+ return false;
156
+ }
157
+ return rclnodejs.isContentFilterSupported(this.handle);
158
+ }
159
+
127
160
  /**
128
161
  * Test if the RMW supports content-filtered topics and that this subscription
129
162
  * has an active wellformed content-filter.
package/lib/timer.js CHANGED
@@ -30,7 +30,8 @@ class Timer {
30
30
  }
31
31
 
32
32
  /**
33
- * @type {bigint} - The period of the timer in nanoseconds.
33
+ * The period of the timer in nanoseconds.
34
+ * @type {bigint}
34
35
  */
35
36
  get period() {
36
37
  return this._period;