rclnodejs 1.8.2 → 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 +62 -23
- package/lib/action/client.js +67 -3
- package/lib/action/server.js +1 -3
- package/lib/distro.js +2 -1
- package/lib/lifecycle_publisher.js +2 -2
- package/lib/message_info.js +94 -0
- package/lib/node.js +90 -14
- package/lib/parameter.js +5 -9
- package/lib/parameter_event_handler.js +468 -0
- package/lib/parameter_watcher.js +12 -12
- package/lib/service.js +8 -4
- package/lib/subscription.js +38 -5
- package/lib/time_source.js +3 -20
- package/lib/timer.js +2 -1
- 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/rosidl_gen/generate_worker.js +3 -13
- package/rosidl_gen/idl_generator.js +210 -0
- package/rosidl_gen/index.js +3 -12
- package/rosidl_gen/packages.js +1 -3
- package/rosidl_gen/primitive_types.js +2 -2
- package/rosidl_parser/idl_parser.py +437 -0
- package/rosidl_parser/parser.py +2 -4
- package/rosidl_parser/rosidl_parser.js +27 -0
- package/scripts/run_asan_test.sh +118 -0
- package/src/executor.cpp +37 -2
- package/src/executor.h +11 -0
- package/src/macros.h +2 -2
- package/src/rcl_action_client_bindings.cpp +88 -12
- package/src/rcl_action_server_bindings.cpp +24 -13
- package/src/rcl_client_bindings.cpp +13 -5
- package/src/rcl_context_bindings.cpp +10 -11
- package/src/rcl_graph_bindings.cpp +2 -2
- package/src/rcl_guard_condition_bindings.cpp +12 -3
- package/src/rcl_lifecycle_bindings.cpp +34 -15
- package/src/rcl_node_bindings.cpp +11 -4
- package/src/rcl_publisher_bindings.cpp +12 -3
- package/src/rcl_service_bindings.cpp +12 -3
- package/src/rcl_subscription_bindings.cpp +92 -21
- package/src/rcl_timer_bindings.cpp +24 -9
- package/src/rcl_type_description_service_bindings.cpp +9 -1
- 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/rosidl_convertor/README.md +0 -298
- package/rosidl_convertor/idl_convertor.js +0 -50
- package/rosidl_convertor/idl_convertor.py +0 -1250
- 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
package/lib/node.js
CHANGED
|
@@ -40,12 +40,14 @@ const {
|
|
|
40
40
|
const ParameterService = require('./parameter_service.js');
|
|
41
41
|
const ParameterClient = require('./parameter_client.js');
|
|
42
42
|
const ParameterWatcher = require('./parameter_watcher.js');
|
|
43
|
+
const ParameterEventHandler = require('./parameter_event_handler.js');
|
|
43
44
|
const Publisher = require('./publisher.js');
|
|
44
45
|
const QoS = require('./qos.js');
|
|
45
46
|
const Rates = require('./rate.js');
|
|
46
47
|
const Service = require('./service.js');
|
|
47
48
|
const Subscription = require('./subscription.js');
|
|
48
49
|
const ObservableSubscription = require('./observable_subscription.js');
|
|
50
|
+
const MessageInfo = require('./message_info.js');
|
|
49
51
|
const TimeSource = require('./time_source.js');
|
|
50
52
|
const Timer = require('./timer.js');
|
|
51
53
|
const TypeDescriptionService = require('./type_description_service.js');
|
|
@@ -97,7 +99,30 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
97
99
|
);
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
static _normalizeOptions(options) {
|
|
103
|
+
if (options instanceof NodeOptions) {
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
const defaults = NodeOptions.defaultOptions;
|
|
107
|
+
return {
|
|
108
|
+
startParameterServices:
|
|
109
|
+
options.startParameterServices ?? defaults.startParameterServices,
|
|
110
|
+
parameterOverrides:
|
|
111
|
+
options.parameterOverrides ?? defaults.parameterOverrides,
|
|
112
|
+
automaticallyDeclareParametersFromOverrides:
|
|
113
|
+
options.automaticallyDeclareParametersFromOverrides ??
|
|
114
|
+
defaults.automaticallyDeclareParametersFromOverrides,
|
|
115
|
+
startTypeDescriptionService:
|
|
116
|
+
options.startTypeDescriptionService ??
|
|
117
|
+
defaults.startTypeDescriptionService,
|
|
118
|
+
enableRosout: options.enableRosout ?? defaults.enableRosout,
|
|
119
|
+
rosoutQos: options.rosoutQos ?? defaults.rosoutQos,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
100
123
|
_init(name, namespace, options, context, args, useGlobalArguments) {
|
|
124
|
+
options = Node._normalizeOptions(options);
|
|
125
|
+
|
|
101
126
|
this.handle = rclnodejs.createNode(
|
|
102
127
|
name,
|
|
103
128
|
namespace,
|
|
@@ -125,6 +150,7 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
125
150
|
this._actionServers = [];
|
|
126
151
|
this._parameterClients = [];
|
|
127
152
|
this._parameterWatchers = [];
|
|
153
|
+
this._parameterEventHandlers = [];
|
|
128
154
|
this._rateTimerServer = null;
|
|
129
155
|
this._parameterDescriptors = new Map();
|
|
130
156
|
this._parameters = new Map();
|
|
@@ -151,7 +177,7 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
151
177
|
// override cli parameterOverrides with those specified in options
|
|
152
178
|
if (options.parameterOverrides.length > 0) {
|
|
153
179
|
for (const parameter of options.parameterOverrides) {
|
|
154
|
-
if ((
|
|
180
|
+
if (!(parameter instanceof Parameter)) {
|
|
155
181
|
throw new TypeValidationError(
|
|
156
182
|
'parameterOverride',
|
|
157
183
|
parameter,
|
|
@@ -246,9 +272,22 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
246
272
|
this._runWithMessageType(
|
|
247
273
|
subscription.typeClass,
|
|
248
274
|
(message, deserialize) => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
275
|
+
if (subscription.wantsMessageInfo) {
|
|
276
|
+
let rawInfo = rclnodejs.rclTakeWithInfo(
|
|
277
|
+
subscription.handle,
|
|
278
|
+
message
|
|
279
|
+
);
|
|
280
|
+
if (rawInfo) {
|
|
281
|
+
subscription.processResponse(
|
|
282
|
+
deserialize(),
|
|
283
|
+
new MessageInfo(rawInfo)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
let success = rclnodejs.rclTake(subscription.handle, message);
|
|
288
|
+
if (success) {
|
|
289
|
+
subscription.processResponse(deserialize());
|
|
290
|
+
}
|
|
252
291
|
}
|
|
253
292
|
}
|
|
254
293
|
);
|
|
@@ -425,17 +464,20 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
425
464
|
}
|
|
426
465
|
|
|
427
466
|
if (properties.isGoalExpired) {
|
|
428
|
-
let
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
467
|
+
let numGoals = actionServer._goalHandles.size;
|
|
468
|
+
if (numGoals > 0) {
|
|
469
|
+
let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
|
|
470
|
+
let message = new GoalInfoArray(numGoals);
|
|
471
|
+
let count = rclnodejs.actionExpireGoals(
|
|
472
|
+
actionServer.handle,
|
|
473
|
+
numGoals,
|
|
474
|
+
message._refArray.buffer
|
|
475
|
+
);
|
|
476
|
+
if (count > 0) {
|
|
477
|
+
actionServer.processGoalExpired(message, count);
|
|
478
|
+
}
|
|
479
|
+
GoalInfoArray.freeArray(message);
|
|
437
480
|
}
|
|
438
|
-
GoalInfoArray.freeArray(message);
|
|
439
481
|
}
|
|
440
482
|
}
|
|
441
483
|
|
|
@@ -1056,6 +1098,7 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
1056
1098
|
|
|
1057
1099
|
this._parameterClients.forEach((paramClient) => paramClient.destroy());
|
|
1058
1100
|
this._parameterWatchers.forEach((watcher) => watcher.destroy());
|
|
1101
|
+
this._parameterEventHandlers.forEach((handler) => handler.destroy());
|
|
1059
1102
|
|
|
1060
1103
|
this.context.onNodeDestroyed(this);
|
|
1061
1104
|
|
|
@@ -1076,6 +1119,7 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
1076
1119
|
this._actionServers = [];
|
|
1077
1120
|
this._parameterClients = [];
|
|
1078
1121
|
this._parameterWatchers = [];
|
|
1122
|
+
this._parameterEventHandlers = [];
|
|
1079
1123
|
|
|
1080
1124
|
if (this._rateTimerServer) {
|
|
1081
1125
|
this._rateTimerServer.shutdown();
|
|
@@ -1188,6 +1232,38 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
1188
1232
|
watcher.destroy();
|
|
1189
1233
|
}
|
|
1190
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* Create a ParameterEventHandler that monitors parameter changes on any node.
|
|
1237
|
+
*
|
|
1238
|
+
* Unlike {@link ParameterWatcher} which watches specific parameters on a single
|
|
1239
|
+
* remote node, ParameterEventHandler can register callbacks for parameters on
|
|
1240
|
+
* any node in the ROS 2 graph by subscribing to /parameter_events.
|
|
1241
|
+
*
|
|
1242
|
+
* @param {object} [options] - Options for the handler
|
|
1243
|
+
* @param {object} [options.qos] - QoS profile for the parameter_events subscription
|
|
1244
|
+
* @return {ParameterEventHandler} - An instance of ParameterEventHandler
|
|
1245
|
+
* @see {@link ParameterEventHandler}
|
|
1246
|
+
*/
|
|
1247
|
+
createParameterEventHandler(options = {}) {
|
|
1248
|
+
const handler = new ParameterEventHandler(this, options);
|
|
1249
|
+
debug('Created ParameterEventHandler on node=%s', this.name());
|
|
1250
|
+
this._parameterEventHandlers.push(handler);
|
|
1251
|
+
return handler;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Destroy a ParameterEventHandler.
|
|
1256
|
+
* @param {ParameterEventHandler} handler - The handler to be destroyed.
|
|
1257
|
+
* @return {undefined}
|
|
1258
|
+
*/
|
|
1259
|
+
destroyParameterEventHandler(handler) {
|
|
1260
|
+
if (!(handler instanceof ParameterEventHandler)) {
|
|
1261
|
+
throw new TypeError('Invalid argument');
|
|
1262
|
+
}
|
|
1263
|
+
this._removeEntityFromArray(handler, this._parameterEventHandlers);
|
|
1264
|
+
handler.destroy();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1191
1267
|
/**
|
|
1192
1268
|
* Destroy a Timer.
|
|
1193
1269
|
* @param {Timer} timer - The Timer to be destroyed.
|
package/lib/parameter.js
CHANGED
|
@@ -864,9 +864,8 @@ function parameterTypeFromValue(value) {
|
|
|
864
864
|
function validType(parameterType) {
|
|
865
865
|
let result =
|
|
866
866
|
typeof parameterType === 'number' &&
|
|
867
|
-
ParameterType.PARAMETER_NOT_SET
|
|
868
|
-
|
|
869
|
-
ParameterType.PARAMETER_STRING_ARRAY;
|
|
867
|
+
parameterType >= ParameterType.PARAMETER_NOT_SET &&
|
|
868
|
+
parameterType <= ParameterType.PARAMETER_STRING_ARRAY;
|
|
870
869
|
|
|
871
870
|
return result;
|
|
872
871
|
}
|
|
@@ -923,14 +922,11 @@ function _validArray(values, type) {
|
|
|
923
922
|
arrayElementType = ParameterType.PARAMETER_BOOL;
|
|
924
923
|
} else if (type === ParameterType.PARAMETER_BYTE_ARRAY) {
|
|
925
924
|
arrayElementType = PARAMETER_BYTE;
|
|
926
|
-
}
|
|
927
|
-
if (type === ParameterType.PARAMETER_INTEGER_ARRAY) {
|
|
925
|
+
} else if (type === ParameterType.PARAMETER_INTEGER_ARRAY) {
|
|
928
926
|
arrayElementType = ParameterType.PARAMETER_INTEGER;
|
|
929
|
-
}
|
|
930
|
-
if (type === ParameterType.PARAMETER_DOUBLE_ARRAY) {
|
|
927
|
+
} else if (type === ParameterType.PARAMETER_DOUBLE_ARRAY) {
|
|
931
928
|
arrayElementType = ParameterType.PARAMETER_DOUBLE;
|
|
932
|
-
}
|
|
933
|
-
if (type === ParameterType.PARAMETER_STRING_ARRAY) {
|
|
929
|
+
} else if (type === ParameterType.PARAMETER_STRING_ARRAY) {
|
|
934
930
|
arrayElementType = ParameterType.PARAMETER_STRING;
|
|
935
931
|
}
|
|
936
932
|
|
|
@@ -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;
|