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.
- package/README.md +46 -37
- package/index.js +29 -0
- package/lib/action/client.js +61 -3
- package/lib/message_info.js +94 -0
- package/lib/node.js +53 -3
- package/lib/parameter_event_handler.js +468 -0
- package/lib/parameter_watcher.js +12 -12
- package/lib/subscription.js +38 -5
- package/lib/timer.js +2 -1
- package/lib/wait_for_message.js +111 -0
- package/package.json +6 -3
- 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/scripts/run_asan_test.sh +118 -0
- package/src/executor.cpp +36 -2
- package/src/executor.h +11 -0
- package/src/rcl_action_client_bindings.cpp +70 -1
- package/src/rcl_context_bindings.cpp +3 -3
- package/src/rcl_graph_bindings.cpp +2 -2
- package/src/rcl_subscription_bindings.cpp +70 -2
- package/src/rcl_utilities.cpp +2 -2
- package/tools/jsdoc/Makefile +5 -0
- package/tools/jsdoc/README.md +96 -0
- package/tools/jsdoc/build-index.js +610 -0
- package/tools/jsdoc/publish.js +854 -0
- package/tools/jsdoc/regenerate-published-docs.js +605 -0
- package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
- package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
- package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
- package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
- package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
- package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
- package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/tools/jsdoc/static/scripts/linenumber.js +25 -0
- package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
- package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
- package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
- package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
- package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
- package/tools/jsdoc/tmpl/augments.tmpl +10 -0
- package/tools/jsdoc/tmpl/container.tmpl +193 -0
- package/tools/jsdoc/tmpl/details.tmpl +143 -0
- package/tools/jsdoc/tmpl/example.tmpl +2 -0
- package/tools/jsdoc/tmpl/examples.tmpl +13 -0
- package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
- package/tools/jsdoc/tmpl/layout.tmpl +83 -0
- package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
- package/tools/jsdoc/tmpl/members.tmpl +43 -0
- package/tools/jsdoc/tmpl/method.tmpl +124 -0
- package/tools/jsdoc/tmpl/params.tmpl +133 -0
- package/tools/jsdoc/tmpl/properties.tmpl +110 -0
- package/tools/jsdoc/tmpl/returns.tmpl +12 -0
- package/tools/jsdoc/tmpl/source.tmpl +8 -0
- package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
- package/tools/jsdoc/tmpl/type.tmpl +7 -0
- package/types/action_client.d.ts +8 -0
- package/types/index.d.ts +34 -0
- package/types/message_info.d.ts +72 -0
- package/types/node.d.ts +21 -0
- package/types/parameter_event_handler.d.ts +139 -0
- package/types/subscription.d.ts +14 -2
- package/test_data_integrity.js +0 -108
- package/test_repro_exact.js +0 -57
- package/test_repro_hz.js +0 -86
- package/test_repro_pub.js +0 -36
- package/test_repro_stress.js +0 -83
- package/test_repro_sub.js +0 -64
- package/test_xproc_data.js +0 -64
- 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;
|
package/lib/parameter_watcher.js
CHANGED
|
@@ -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
|
-
#
|
|
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.#
|
|
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.#
|
|
137
|
-
this.#
|
|
138
|
-
|
|
139
|
-
|
|
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.#
|
|
230
|
+
if (this.#eventHandler) {
|
|
231
231
|
try {
|
|
232
|
-
this.#
|
|
232
|
+
this.#eventHandler.destroy();
|
|
233
233
|
} catch (error) {
|
|
234
|
-
debug('Error destroying
|
|
234
|
+
debug('Error destroying event handler: %s', error.message);
|
|
235
235
|
}
|
|
236
|
-
this.#
|
|
236
|
+
this.#eventHandler = null;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
if (this.#paramClient) {
|
package/lib/subscription.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|