rclnodejs 1.8.3 → 1.9.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 +39 -0
- package/lib/action/client.js +61 -3
- package/lib/action/server_goal_handle.js +26 -1
- package/lib/message_info.js +94 -0
- package/lib/node.js +260 -13
- package/lib/parameter_event_handler.js +566 -0
- package/lib/parameter_watcher.js +12 -12
- package/lib/qos_overriding_options.js +358 -0
- package/lib/subscription.js +38 -5
- package/lib/timer.js +3 -2
- package/lib/wait_for_message.js +111 -0
- package/package.json +7 -4
- 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_timer_bindings.cpp +21 -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 +90 -5
- package/types/parameter_event_handler.d.ts +150 -0
- package/types/qos.d.ts +55 -0
- package/types/subscription.d.ts +14 -2
- package/types/timer.d.ts +3 -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,566 @@
|
|
|
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 validator = require('./validator');
|
|
20
|
+
const debug = require('debug')('rclnodejs:parameter_event_handler');
|
|
21
|
+
|
|
22
|
+
const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
|
|
23
|
+
const PARAMETER_EVENT_TOPIC = '/parameter_events';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @class ParameterCallbackHandle
|
|
27
|
+
* Opaque handle returned when adding a parameter callback.
|
|
28
|
+
* Used to remove the callback later.
|
|
29
|
+
*/
|
|
30
|
+
class ParameterCallbackHandle {
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} parameterName - The parameter name
|
|
33
|
+
* @param {string} nodeName - The fully qualified node name
|
|
34
|
+
* @param {Function} callback - The callback function
|
|
35
|
+
* @hideconstructor
|
|
36
|
+
*/
|
|
37
|
+
constructor(parameterName, nodeName, callback) {
|
|
38
|
+
this.parameterName = parameterName;
|
|
39
|
+
this.nodeName = nodeName;
|
|
40
|
+
this.callback = callback;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @class ParameterEventCallbackHandle
|
|
46
|
+
* Opaque handle returned when adding a parameter event callback.
|
|
47
|
+
* Used to remove the callback later.
|
|
48
|
+
*/
|
|
49
|
+
class ParameterEventCallbackHandle {
|
|
50
|
+
/**
|
|
51
|
+
* @param {Function} callback - The callback function
|
|
52
|
+
* @hideconstructor
|
|
53
|
+
*/
|
|
54
|
+
constructor(callback) {
|
|
55
|
+
this.callback = callback;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @class ParameterEventHandler
|
|
61
|
+
*
|
|
62
|
+
* Monitors and responds to parameter changes on any node in the ROS 2 graph
|
|
63
|
+
* by subscribing to the `/parameter_events` topic.
|
|
64
|
+
*
|
|
65
|
+
* Unlike {@link ParameterWatcher}, which is tied to a single remote node and
|
|
66
|
+
* requires waiting for that node's parameter services, ParameterEventHandler
|
|
67
|
+
* responds to parameter events from any node without needing service availability.
|
|
68
|
+
*
|
|
69
|
+
* Two types of callbacks are supported:
|
|
70
|
+
* - **Parameter callbacks**: fired when a specific parameter on a specific node
|
|
71
|
+
* is added or changed (new_parameters + changed_parameters).
|
|
72
|
+
* Note: deleted parameters are not dispatched to parameter callbacks;
|
|
73
|
+
* use event callbacks to observe deletions.
|
|
74
|
+
* - **Event callbacks**: fired for every ParameterEvent message received,
|
|
75
|
+
* including deletions.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const handler = node.createParameterEventHandler();
|
|
79
|
+
*
|
|
80
|
+
* // Watch a specific parameter on a specific node
|
|
81
|
+
* const handle = handler.addParameterCallback(
|
|
82
|
+
* 'my_param',
|
|
83
|
+
* '/my_node',
|
|
84
|
+
* (parameter) => {
|
|
85
|
+
* console.log(`Parameter changed: ${parameter.name} = ${parameter.value}`);
|
|
86
|
+
* }
|
|
87
|
+
* );
|
|
88
|
+
*
|
|
89
|
+
* // Watch all parameter events
|
|
90
|
+
* const eventHandle = handler.addParameterEventCallback((event) => {
|
|
91
|
+
* console.log(`Event from node: ${event.node}`);
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* // Remove callbacks when done
|
|
95
|
+
* handler.removeParameterCallback(handle);
|
|
96
|
+
* handler.removeParameterEventCallback(eventHandle);
|
|
97
|
+
*
|
|
98
|
+
* // Destroy when no longer needed
|
|
99
|
+
* handler.destroy();
|
|
100
|
+
*/
|
|
101
|
+
class ParameterEventHandler {
|
|
102
|
+
#node;
|
|
103
|
+
#subscription;
|
|
104
|
+
#parameterCallbacks; // Map<string, ParameterCallbackHandle[]> keyed by "paramName\0nodeName"
|
|
105
|
+
#eventCallbacks; // ParameterEventCallbackHandle[]
|
|
106
|
+
#destroyed;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a ParameterEventHandler.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} node - The rclnodejs Node used to create the subscription
|
|
112
|
+
* @param {object} [options] - Options
|
|
113
|
+
* @param {object} [options.qos] - QoS profile for the parameter_events subscription
|
|
114
|
+
*/
|
|
115
|
+
constructor(node, options = {}) {
|
|
116
|
+
if (!node || typeof node.createSubscription !== 'function') {
|
|
117
|
+
throw new TypeValidationError('node', node, 'Node instance', {
|
|
118
|
+
entityType: 'parameter event handler',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
options !== undefined &&
|
|
124
|
+
options !== null &&
|
|
125
|
+
typeof options !== 'object'
|
|
126
|
+
) {
|
|
127
|
+
throw new TypeValidationError('options', options, 'object or undefined', {
|
|
128
|
+
entityType: 'parameter event handler',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const opts = options || {};
|
|
133
|
+
|
|
134
|
+
this.#node = node;
|
|
135
|
+
this.#parameterCallbacks = new Map();
|
|
136
|
+
this.#eventCallbacks = [];
|
|
137
|
+
this.#destroyed = false;
|
|
138
|
+
|
|
139
|
+
const subscriptionOptions = opts.qos ? { qos: opts.qos } : undefined;
|
|
140
|
+
|
|
141
|
+
this.#subscription = node.createSubscription(
|
|
142
|
+
PARAMETER_EVENT_MSG_TYPE,
|
|
143
|
+
PARAMETER_EVENT_TOPIC,
|
|
144
|
+
subscriptionOptions,
|
|
145
|
+
(event) => this.#handleEvent(event)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
debug('Created ParameterEventHandler on node=%s', node.name());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Add a callback for a specific parameter on a specific node.
|
|
153
|
+
*
|
|
154
|
+
* The callback is invoked whenever the named parameter is added or changed
|
|
155
|
+
* on the specified node. The callback receives the parameter message object
|
|
156
|
+
* (rcl_interfaces/msg/Parameter) with `name` and `value` fields.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} parameterName - Name of the parameter to monitor
|
|
159
|
+
* @param {string} nodeName - Fully qualified name of the node (e.g., '/my_node')
|
|
160
|
+
* @param {Function} callback - Called with (parameter) when the parameter changes
|
|
161
|
+
* @returns {ParameterCallbackHandle} Handle for removing this callback later
|
|
162
|
+
* @throws {Error} If the handler has been destroyed
|
|
163
|
+
* @throws {TypeError} If arguments are invalid
|
|
164
|
+
*/
|
|
165
|
+
addParameterCallback(parameterName, nodeName, callback) {
|
|
166
|
+
this.#checkNotDestroyed();
|
|
167
|
+
|
|
168
|
+
if (typeof parameterName !== 'string' || parameterName.trim() === '') {
|
|
169
|
+
throw new TypeValidationError(
|
|
170
|
+
'parameterName',
|
|
171
|
+
parameterName,
|
|
172
|
+
'non-empty string',
|
|
173
|
+
{ entityType: 'parameter event handler' }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof nodeName !== 'string' || nodeName.trim() === '') {
|
|
178
|
+
throw new TypeValidationError('nodeName', nodeName, 'non-empty string', {
|
|
179
|
+
entityType: 'parameter event handler',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (typeof callback !== 'function') {
|
|
184
|
+
throw new TypeValidationError('callback', callback, 'function', {
|
|
185
|
+
entityType: 'parameter event handler',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const resolvedNodeName = normalizeNodeName(nodeName);
|
|
190
|
+
const resolvedParamName = parameterName.trim();
|
|
191
|
+
const handle = new ParameterCallbackHandle(
|
|
192
|
+
resolvedParamName,
|
|
193
|
+
resolvedNodeName,
|
|
194
|
+
callback
|
|
195
|
+
);
|
|
196
|
+
const key = this.#makeKey(resolvedParamName, resolvedNodeName);
|
|
197
|
+
|
|
198
|
+
if (!this.#parameterCallbacks.has(key)) {
|
|
199
|
+
this.#parameterCallbacks.set(key, []);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Insert at front (FILO order, matching rclpy behavior)
|
|
203
|
+
this.#parameterCallbacks.get(key).unshift(handle);
|
|
204
|
+
|
|
205
|
+
debug(
|
|
206
|
+
'Added parameter callback: param=%s node=%s',
|
|
207
|
+
resolvedParamName,
|
|
208
|
+
resolvedNodeName
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return handle;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Configure which node parameter events will be received.
|
|
216
|
+
*
|
|
217
|
+
* If nodeNames is omitted or empty, the current node filter is cleared.
|
|
218
|
+
* When a filter is active, parameter and event callbacks only receive
|
|
219
|
+
* events from the specified nodes.
|
|
220
|
+
*
|
|
221
|
+
* @param {string[]} [nodeNames] - Node names to filter parameter events from.
|
|
222
|
+
* Relative names are resolved against the handler node namespace.
|
|
223
|
+
* @returns {boolean} True if the filter is active or was successfully cleared.
|
|
224
|
+
*/
|
|
225
|
+
configureNodesFilter(nodeNames) {
|
|
226
|
+
this.#checkNotDestroyed();
|
|
227
|
+
|
|
228
|
+
if (nodeNames === undefined || nodeNames === null) {
|
|
229
|
+
this.#subscription.clearContentFilter();
|
|
230
|
+
return !this.#subscription.hasContentFilter();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!Array.isArray(nodeNames)) {
|
|
234
|
+
throw new TypeValidationError('nodeNames', nodeNames, 'string[]', {
|
|
235
|
+
entityType: 'parameter event handler',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (nodeNames.length === 0) {
|
|
240
|
+
this.#subscription.clearContentFilter();
|
|
241
|
+
return !this.#subscription.hasContentFilter();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const resolvedNodeNames = nodeNames.map((nodeName, index) => {
|
|
245
|
+
if (typeof nodeName !== 'string' || nodeName.trim() === '') {
|
|
246
|
+
throw new TypeValidationError(
|
|
247
|
+
`nodeNames[${index}]`,
|
|
248
|
+
nodeName,
|
|
249
|
+
'non-empty string',
|
|
250
|
+
{
|
|
251
|
+
entityType: 'parameter event handler',
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const resolvedNodeName = this.#resolvePath(nodeName.trim());
|
|
257
|
+
this.#validateFullyQualifiedNodePath(resolvedNodeName);
|
|
258
|
+
return resolvedNodeName;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const contentFilter = {
|
|
262
|
+
expression: resolvedNodeNames
|
|
263
|
+
.map((_, index) => `node = %${index}`)
|
|
264
|
+
.join(' OR '),
|
|
265
|
+
parameters: resolvedNodeNames.map((nodeName) => `'${nodeName}'`),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
this.#subscription.setContentFilter(contentFilter);
|
|
269
|
+
return this.#subscription.hasContentFilter();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Remove a previously added parameter callback.
|
|
274
|
+
*
|
|
275
|
+
* @param {ParameterCallbackHandle} handle - The handle returned by addParameterCallback
|
|
276
|
+
* @throws {Error} If the handle is not found or handler is destroyed
|
|
277
|
+
*/
|
|
278
|
+
removeParameterCallback(handle) {
|
|
279
|
+
this.#checkNotDestroyed();
|
|
280
|
+
|
|
281
|
+
if (!(handle instanceof ParameterCallbackHandle)) {
|
|
282
|
+
throw new TypeValidationError(
|
|
283
|
+
'handle',
|
|
284
|
+
handle,
|
|
285
|
+
'ParameterCallbackHandle',
|
|
286
|
+
{ entityType: 'parameter event handler' }
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const key = this.#makeKey(handle.parameterName, handle.nodeName);
|
|
291
|
+
const callbacks = this.#parameterCallbacks.get(key);
|
|
292
|
+
|
|
293
|
+
if (!callbacks) {
|
|
294
|
+
throw new OperationError(
|
|
295
|
+
`No callbacks registered for parameter '${handle.parameterName}' on node '${handle.nodeName}'`,
|
|
296
|
+
{ entityType: 'parameter event handler' }
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const index = callbacks.indexOf(handle);
|
|
301
|
+
if (index === -1) {
|
|
302
|
+
throw new OperationError("Callback doesn't exist", {
|
|
303
|
+
entityType: 'parameter event handler',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
callbacks.splice(index, 1);
|
|
308
|
+
|
|
309
|
+
if (callbacks.length === 0) {
|
|
310
|
+
this.#parameterCallbacks.delete(key);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
debug(
|
|
314
|
+
'Removed parameter callback: param=%s node=%s',
|
|
315
|
+
handle.parameterName,
|
|
316
|
+
handle.nodeName
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Add a callback that is invoked for every parameter event.
|
|
322
|
+
*
|
|
323
|
+
* The callback receives the full ParameterEvent message
|
|
324
|
+
* (rcl_interfaces/msg/ParameterEvent) with `node`, `new_parameters`,
|
|
325
|
+
* `changed_parameters`, and `deleted_parameters` fields.
|
|
326
|
+
*
|
|
327
|
+
* @param {Function} callback - Called with (event) for every ParameterEvent
|
|
328
|
+
* @returns {ParameterEventCallbackHandle} Handle for removing this callback later
|
|
329
|
+
* @throws {Error} If the handler has been destroyed
|
|
330
|
+
* @throws {TypeError} If callback is not a function
|
|
331
|
+
*/
|
|
332
|
+
addParameterEventCallback(callback) {
|
|
333
|
+
this.#checkNotDestroyed();
|
|
334
|
+
|
|
335
|
+
if (typeof callback !== 'function') {
|
|
336
|
+
throw new TypeValidationError('callback', callback, 'function', {
|
|
337
|
+
entityType: 'parameter event handler',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const handle = new ParameterEventCallbackHandle(callback);
|
|
342
|
+
|
|
343
|
+
// Insert at front (FILO order)
|
|
344
|
+
this.#eventCallbacks.unshift(handle);
|
|
345
|
+
|
|
346
|
+
debug('Added parameter event callback');
|
|
347
|
+
|
|
348
|
+
return handle;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Remove a previously added parameter event callback.
|
|
353
|
+
*
|
|
354
|
+
* @param {ParameterEventCallbackHandle} handle - The handle returned by addParameterEventCallback
|
|
355
|
+
* @throws {Error} If the handle is not found or handler is destroyed
|
|
356
|
+
*/
|
|
357
|
+
removeParameterEventCallback(handle) {
|
|
358
|
+
this.#checkNotDestroyed();
|
|
359
|
+
|
|
360
|
+
if (!(handle instanceof ParameterEventCallbackHandle)) {
|
|
361
|
+
throw new TypeValidationError(
|
|
362
|
+
'handle',
|
|
363
|
+
handle,
|
|
364
|
+
'ParameterEventCallbackHandle',
|
|
365
|
+
{ entityType: 'parameter event handler' }
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const index = this.#eventCallbacks.indexOf(handle);
|
|
370
|
+
if (index === -1) {
|
|
371
|
+
throw new OperationError("Callback doesn't exist", {
|
|
372
|
+
entityType: 'parameter event handler',
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.#eventCallbacks.splice(index, 1);
|
|
377
|
+
|
|
378
|
+
debug('Removed parameter event callback');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Check if the handler has been destroyed.
|
|
383
|
+
*
|
|
384
|
+
* @returns {boolean} True if destroyed
|
|
385
|
+
*/
|
|
386
|
+
isDestroyed() {
|
|
387
|
+
return this.#destroyed;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Destroy the handler and clean up resources.
|
|
392
|
+
* Removes the subscription and clears all callbacks.
|
|
393
|
+
*/
|
|
394
|
+
destroy() {
|
|
395
|
+
if (this.#destroyed) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
debug('Destroying ParameterEventHandler');
|
|
400
|
+
|
|
401
|
+
if (this.#subscription) {
|
|
402
|
+
try {
|
|
403
|
+
this.#node.destroySubscription(this.#subscription);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
debug('Error destroying subscription: %s', error.message);
|
|
406
|
+
}
|
|
407
|
+
this.#subscription = null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.#parameterCallbacks.clear();
|
|
411
|
+
this.#eventCallbacks.length = 0;
|
|
412
|
+
this.#destroyed = true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get a specific parameter from a ParameterEvent message.
|
|
417
|
+
*
|
|
418
|
+
* @param {object} event - A ParameterEvent message
|
|
419
|
+
* @param {string} parameterName - The parameter name to look for
|
|
420
|
+
* @param {string} nodeName - The node name to match
|
|
421
|
+
* @returns {object|null} The matching parameter message, or null
|
|
422
|
+
* @static
|
|
423
|
+
*/
|
|
424
|
+
static getParameterFromEvent(event, parameterName, nodeName) {
|
|
425
|
+
const resolvedNodeName = normalizeNodeName(nodeName);
|
|
426
|
+
const resolvedParamName = (parameterName || '').trim();
|
|
427
|
+
|
|
428
|
+
if (normalizeNodeName(event.node) !== resolvedNodeName) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const allParams = [
|
|
433
|
+
...(event.new_parameters || []),
|
|
434
|
+
...(event.changed_parameters || []),
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
for (const param of allParams) {
|
|
438
|
+
if (param.name === resolvedParamName) {
|
|
439
|
+
return param;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get all parameters from a ParameterEvent message (new + changed).
|
|
448
|
+
*
|
|
449
|
+
* @param {object} event - A ParameterEvent message
|
|
450
|
+
* @returns {object[]} Array of parameter messages
|
|
451
|
+
* @static
|
|
452
|
+
*/
|
|
453
|
+
static getParametersFromEvent(event) {
|
|
454
|
+
return [
|
|
455
|
+
...(event.new_parameters || []),
|
|
456
|
+
...(event.changed_parameters || []),
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Handle incoming parameter event.
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
#handleEvent(event) {
|
|
465
|
+
const eventNodeName = normalizeNodeName(event.node);
|
|
466
|
+
|
|
467
|
+
// Dispatch parameter-specific callbacks by iterating event params
|
|
468
|
+
// and doing direct Map lookups (O(event_params) instead of O(registered_callbacks))
|
|
469
|
+
const allParams = [
|
|
470
|
+
...(event.new_parameters || []),
|
|
471
|
+
...(event.changed_parameters || []),
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
for (const parameter of allParams) {
|
|
475
|
+
const key = this.#makeKey(parameter.name, eventNodeName);
|
|
476
|
+
const callbacks = this.#parameterCallbacks.get(key);
|
|
477
|
+
|
|
478
|
+
if (callbacks) {
|
|
479
|
+
for (const handle of callbacks.slice()) {
|
|
480
|
+
try {
|
|
481
|
+
handle.callback(parameter);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
debug(
|
|
484
|
+
'Error in parameter callback for %s on %s: %s',
|
|
485
|
+
parameter.name,
|
|
486
|
+
eventNodeName,
|
|
487
|
+
err.message
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Dispatch event-level callbacks
|
|
495
|
+
for (const handle of this.#eventCallbacks.slice()) {
|
|
496
|
+
try {
|
|
497
|
+
handle.callback(event);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
debug('Error in parameter event callback: %s', err.message);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Create a map key from parameter name and node name.
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
#makeKey(paramName, nodeName) {
|
|
509
|
+
return `${paramName}\0${nodeName}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Resolve a node path to the fully qualified name used in ParameterEvent.node.
|
|
514
|
+
* @private
|
|
515
|
+
*/
|
|
516
|
+
#resolvePath(nodePath) {
|
|
517
|
+
// Absolute node paths are already rooted. Relative names are resolved
|
|
518
|
+
// against the handler node namespace before building the content filter.
|
|
519
|
+
const unresolvedPath = nodePath.startsWith('/')
|
|
520
|
+
? nodePath
|
|
521
|
+
: `${this.#node.namespace().replace(/\/+$/, '')}/${nodePath}`;
|
|
522
|
+
|
|
523
|
+
// Collapse repeated separators for inputs like '/ns//node/' or 'nested//node'.
|
|
524
|
+
const resolvedPath = unresolvedPath.replace(/\/+/g, '/');
|
|
525
|
+
|
|
526
|
+
// Preserve the root namespace as '/' and strip trailing slashes everywhere
|
|
527
|
+
// else so the filter matches the canonical ParameterEvent.node format.
|
|
528
|
+
if (resolvedPath === '/') {
|
|
529
|
+
return resolvedPath;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return resolvedPath.replace(/\/+$/, '');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Validate a fully qualified node path before using it in a content filter.
|
|
537
|
+
* @private
|
|
538
|
+
*/
|
|
539
|
+
#validateFullyQualifiedNodePath(nodePath) {
|
|
540
|
+
const normalizedPath =
|
|
541
|
+
nodePath.length > 1 ? nodePath.replace(/\/+$/, '') : nodePath;
|
|
542
|
+
const separatorIndex = normalizedPath.lastIndexOf('/');
|
|
543
|
+
const nodeNamespace =
|
|
544
|
+
separatorIndex === 0 ? '/' : normalizedPath.slice(0, separatorIndex);
|
|
545
|
+
const nodeName = normalizedPath.slice(separatorIndex + 1);
|
|
546
|
+
|
|
547
|
+
validator.validateNamespace(nodeNamespace);
|
|
548
|
+
validator.validateNodeName(nodeName);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Check if the handler has been destroyed and throw if so.
|
|
553
|
+
* @private
|
|
554
|
+
*/
|
|
555
|
+
#checkNotDestroyed() {
|
|
556
|
+
if (this.#destroyed) {
|
|
557
|
+
throw new OperationError('ParameterEventHandler has been destroyed', {
|
|
558
|
+
entityType: 'parameter event handler',
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
module.exports = ParameterEventHandler;
|
|
565
|
+
module.exports.ParameterCallbackHandle = ParameterCallbackHandle;
|
|
566
|
+
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) {
|