rclnodejs 1.9.0-alpha.0 → 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/index.js +10 -0
- package/lib/action/server_goal_handle.js +26 -1
- package/lib/node.js +207 -10
- package/lib/parameter_event_handler.js +98 -0
- package/lib/qos_overriding_options.js +358 -0
- package/lib/timer.js +1 -1
- package/package.json +2 -2
- 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/src/rcl_timer_bindings.cpp +21 -2
- package/types/node.d.ts +69 -5
- package/types/parameter_event_handler.d.ts +11 -0
- package/types/qos.d.ts +55 -0
- package/types/timer.d.ts +3 -2
package/index.js
CHANGED
|
@@ -39,6 +39,10 @@ const {
|
|
|
39
39
|
} = require('./lib/parameter.js');
|
|
40
40
|
const path = require('path');
|
|
41
41
|
const QoS = require('./lib/qos.js');
|
|
42
|
+
const {
|
|
43
|
+
QoSPolicyKind,
|
|
44
|
+
QoSOverridingOptions,
|
|
45
|
+
} = require('./lib/qos_overriding_options.js');
|
|
42
46
|
const rclnodejs = require('./lib/native_loader.js');
|
|
43
47
|
const tsdGenerator = require('./rostsd_gen/index.js');
|
|
44
48
|
const validator = require('./lib/validator.js');
|
|
@@ -258,6 +262,12 @@ let rcl = {
|
|
|
258
262
|
/** {@link QoS} class */
|
|
259
263
|
QoS: QoS,
|
|
260
264
|
|
|
265
|
+
/** {@link QoSPolicyKind} enum */
|
|
266
|
+
QoSPolicyKind: QoSPolicyKind,
|
|
267
|
+
|
|
268
|
+
/** {@link QoSOverridingOptions} class */
|
|
269
|
+
QoSOverridingOptions: QoSOverridingOptions,
|
|
270
|
+
|
|
261
271
|
/** {@link RMWUtils} */
|
|
262
272
|
RMWUtils: RMWUtils,
|
|
263
273
|
|
|
@@ -82,11 +82,26 @@ class ServerGoalHandle {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
85
|
+
* Transitions the goal to the executing state and begins execution.
|
|
86
|
+
* Has no effect if the goal is already executing, no longer active, or destroyed.
|
|
86
87
|
* @param {function} callback - An optional callback to use instead of the one provided to the action server.
|
|
87
88
|
* @returns {undefined}
|
|
88
89
|
*/
|
|
89
90
|
execute(callback) {
|
|
91
|
+
if (this._destroyed) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Guard: already executing — no-op
|
|
96
|
+
if (this.status === ActionInterfaces.GoalStatus.STATUS_EXECUTING) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Guard: only transition if goal is still active
|
|
101
|
+
if (!this.isActive) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
if (!this.isCancelRequested) {
|
|
91
106
|
this._updateState(GoalEvent.EXECUTE);
|
|
92
107
|
}
|
|
@@ -105,6 +120,16 @@ class ServerGoalHandle {
|
|
|
105
120
|
return;
|
|
106
121
|
}
|
|
107
122
|
|
|
123
|
+
if (this.status !== ActionInterfaces.GoalStatus.STATUS_EXECUTING) {
|
|
124
|
+
this._actionServer._node
|
|
125
|
+
.getLogger()
|
|
126
|
+
.warn(
|
|
127
|
+
`publishFeedback() called on goal ${this.goalId.uuid} ` +
|
|
128
|
+
`in status ${this.status}, not executing; ignoring`
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
let feedbackMessage =
|
|
109
134
|
new this._actionServer.typeClass.impl.FeedbackMessage();
|
|
110
135
|
feedbackMessage['goal_id'] = this.goalId;
|
package/lib/node.js
CHANGED
|
@@ -48,6 +48,10 @@ const Service = require('./service.js');
|
|
|
48
48
|
const Subscription = require('./subscription.js');
|
|
49
49
|
const ObservableSubscription = require('./observable_subscription.js');
|
|
50
50
|
const MessageInfo = require('./message_info.js');
|
|
51
|
+
const {
|
|
52
|
+
declareQosParameters,
|
|
53
|
+
_resolveQoS,
|
|
54
|
+
} = require('./qos_overriding_options.js');
|
|
51
55
|
const TimeSource = require('./time_source.js');
|
|
52
56
|
const Timer = require('./timer.js');
|
|
53
57
|
const TypeDescriptionService = require('./type_description_service.js');
|
|
@@ -157,7 +161,9 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
157
161
|
this._parameterService = null;
|
|
158
162
|
this._typeDescriptionService = null;
|
|
159
163
|
this._parameterEventPublisher = null;
|
|
164
|
+
this._preSetParametersCallbacks = [];
|
|
160
165
|
this._setParametersCallbacks = [];
|
|
166
|
+
this._postSetParametersCallbacks = [];
|
|
161
167
|
this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
|
|
162
168
|
this._spinning = false;
|
|
163
169
|
this._enableRosout = options.enableRosout;
|
|
@@ -250,8 +256,14 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
250
256
|
|
|
251
257
|
timersReady.forEach((timer) => {
|
|
252
258
|
if (timer.isReady()) {
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
let timerInfo;
|
|
260
|
+
if (typeof rclnodejs.callTimerWithInfo === 'function') {
|
|
261
|
+
timerInfo = rclnodejs.callTimerWithInfo(timer.handle);
|
|
262
|
+
timer.callback(timerInfo);
|
|
263
|
+
} else {
|
|
264
|
+
rclnodejs.callTimer(timer.handle);
|
|
265
|
+
timer.callback();
|
|
266
|
+
}
|
|
255
267
|
}
|
|
256
268
|
});
|
|
257
269
|
|
|
@@ -622,15 +634,43 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
622
634
|
/**
|
|
623
635
|
* Create a Timer.
|
|
624
636
|
* @param {bigint} period - The number representing period in nanoseconds.
|
|
625
|
-
* @param {function} callback - The callback to be called when
|
|
626
|
-
*
|
|
637
|
+
* @param {function} callback - The callback to be called when the timer fires.
|
|
638
|
+
* On distros with native support, the callback receives a `TimerInfo` object
|
|
639
|
+
* describing the expected and actual call time.
|
|
640
|
+
* @param {object|Clock} [optionsOrClock] - Timer options or the clock which the timer gets time from.
|
|
641
|
+
* Supported options: `{ autostart?: boolean }`.
|
|
642
|
+
* @param {Clock} [clock] - The clock which the timer gets time from when options are provided.
|
|
627
643
|
* @return {Timer} - An instance of Timer.
|
|
628
644
|
*/
|
|
629
|
-
createTimer(period, callback, clock = null) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
clock =
|
|
645
|
+
createTimer(period, callback, optionsOrClock = null, clock = null) {
|
|
646
|
+
let options = {};
|
|
647
|
+
|
|
648
|
+
if (optionsOrClock instanceof Clock.Clock) {
|
|
649
|
+
clock = optionsOrClock;
|
|
650
|
+
} else if (optionsOrClock === null || optionsOrClock === undefined) {
|
|
651
|
+
// Keep the 4th argument as the clock when the 3rd argument is omitted or explicitly null.
|
|
652
|
+
} else {
|
|
653
|
+
if (typeof optionsOrClock !== 'object' || Array.isArray(optionsOrClock)) {
|
|
654
|
+
throw new TypeValidationError(
|
|
655
|
+
'options',
|
|
656
|
+
optionsOrClock,
|
|
657
|
+
'object or Clock',
|
|
658
|
+
{
|
|
659
|
+
nodeName: this.name(),
|
|
660
|
+
}
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
options = optionsOrClock;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (
|
|
667
|
+
arguments.length === 4 &&
|
|
668
|
+
clock !== null &&
|
|
669
|
+
!(clock instanceof Clock.Clock)
|
|
670
|
+
) {
|
|
671
|
+
throw new TypeValidationError('clock', clock, 'Clock', {
|
|
672
|
+
nodeName: this.name(),
|
|
673
|
+
});
|
|
634
674
|
}
|
|
635
675
|
|
|
636
676
|
if (typeof period !== 'bigint') {
|
|
@@ -643,12 +683,27 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
643
683
|
nodeName: this.name(),
|
|
644
684
|
});
|
|
645
685
|
}
|
|
686
|
+
if (
|
|
687
|
+
options.autostart !== undefined &&
|
|
688
|
+
typeof options.autostart !== 'boolean'
|
|
689
|
+
) {
|
|
690
|
+
throw new TypeValidationError(
|
|
691
|
+
'options.autostart',
|
|
692
|
+
options.autostart,
|
|
693
|
+
'boolean',
|
|
694
|
+
{
|
|
695
|
+
nodeName: this.name(),
|
|
696
|
+
}
|
|
697
|
+
);
|
|
698
|
+
}
|
|
646
699
|
|
|
647
700
|
const timerClock = clock || this._clock;
|
|
701
|
+
const autostart = options.autostart ?? true;
|
|
648
702
|
let timerHandle = rclnodejs.createTimer(
|
|
649
703
|
timerClock.handle,
|
|
650
704
|
this.context.handle,
|
|
651
|
-
period
|
|
705
|
+
period,
|
|
706
|
+
autostart
|
|
652
707
|
);
|
|
653
708
|
let timer = new Timer(timerHandle, period, callback);
|
|
654
709
|
debug('Finish creating timer, period = %d.', period);
|
|
@@ -705,6 +760,10 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
705
760
|
* @param {object} options - The options argument used to parameterize the publisher.
|
|
706
761
|
* @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
|
|
707
762
|
* @param {QoS} options.qos - ROS Middleware "quality of service" settings for the publisher, default: QoS.profileDefault.
|
|
763
|
+
* @param {QoSOverridingOptions} [options.qosOverridingOptions] - If provided, declares read-only ROS parameters
|
|
764
|
+
* for the specified QoS policies (e.g. `qos_overrides./topic.publisher.depth`). These can be overridden at
|
|
765
|
+
* startup via `--ros-args -p` or `--params-file`. If qos is a profile string, it will be resolved to a
|
|
766
|
+
* mutable QoS object before overrides are applied.
|
|
708
767
|
* @param {PublisherEventCallbacks} eventCallbacks - The event callbacks for the publisher.
|
|
709
768
|
* @return {Publisher} - An instance of Publisher.
|
|
710
769
|
*/
|
|
@@ -752,6 +811,21 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
752
811
|
);
|
|
753
812
|
}
|
|
754
813
|
|
|
814
|
+
// Apply QoS overriding options if provided
|
|
815
|
+
if (options.qosOverridingOptions) {
|
|
816
|
+
const resolvedTopic = this.resolveTopicName(topic);
|
|
817
|
+
if (typeof options.qos === 'string' || !(options.qos instanceof QoS)) {
|
|
818
|
+
options.qos = _resolveQoS(options.qos);
|
|
819
|
+
}
|
|
820
|
+
declareQosParameters(
|
|
821
|
+
'publisher',
|
|
822
|
+
this,
|
|
823
|
+
resolvedTopic,
|
|
824
|
+
options.qos,
|
|
825
|
+
options.qosOverridingOptions
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
755
829
|
let publisher = publisherClass.createPublisher(
|
|
756
830
|
this,
|
|
757
831
|
typeClass,
|
|
@@ -795,6 +869,10 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
795
869
|
* @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
|
|
796
870
|
* the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
|
|
797
871
|
* fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
|
|
872
|
+
* @param {QoSOverridingOptions} [options.qosOverridingOptions] - If provided, declares read-only ROS parameters
|
|
873
|
+
* for the specified QoS policies (e.g. `qos_overrides./topic.subscription.depth`). These can be overridden at
|
|
874
|
+
* startup via `--ros-args -p` or `--params-file`. If qos is a profile string, it will be resolved to a
|
|
875
|
+
* mutable QoS object before overrides are applied.
|
|
798
876
|
* @param {SubscriptionCallback} callback - The callback to be call when receiving the topic subscribed. The topic will be an instance of null-terminated Buffer when options.isRaw is true.
|
|
799
877
|
* @param {SubscriptionEventCallbacks} eventCallbacks - The event callbacks for the subscription.
|
|
800
878
|
* @return {Subscription} - An instance of Subscription.
|
|
@@ -848,6 +926,21 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
848
926
|
);
|
|
849
927
|
}
|
|
850
928
|
|
|
929
|
+
// Apply QoS overriding options if provided
|
|
930
|
+
if (options.qosOverridingOptions) {
|
|
931
|
+
const resolvedTopic = this.resolveTopicName(topic);
|
|
932
|
+
if (typeof options.qos === 'string' || !(options.qos instanceof QoS)) {
|
|
933
|
+
options.qos = _resolveQoS(options.qos);
|
|
934
|
+
}
|
|
935
|
+
declareQosParameters(
|
|
936
|
+
'subscription',
|
|
937
|
+
this,
|
|
938
|
+
resolvedTopic,
|
|
939
|
+
options.qos,
|
|
940
|
+
options.qosOverridingOptions
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
851
944
|
let subscription = Subscription.createSubscription(
|
|
852
945
|
this,
|
|
853
946
|
typeClass,
|
|
@@ -2015,6 +2108,29 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
2015
2108
|
* @return {SetParameterResult} - A single collective result.
|
|
2016
2109
|
*/
|
|
2017
2110
|
_setParametersAtomically(parameters = [], declareParameterMode = false) {
|
|
2111
|
+
// 1) PRE callbacks — pipeline: each callback receives the output of the previous
|
|
2112
|
+
if (this._preSetParametersCallbacks.length > 0) {
|
|
2113
|
+
for (const callback of this._preSetParametersCallbacks) {
|
|
2114
|
+
const result = callback(parameters);
|
|
2115
|
+
if (!Array.isArray(result)) {
|
|
2116
|
+
return {
|
|
2117
|
+
successful: false,
|
|
2118
|
+
reason:
|
|
2119
|
+
'pre-set parameters callback must return an array of Parameters',
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
parameters = result;
|
|
2123
|
+
if (parameters.length === 0) {
|
|
2124
|
+
return {
|
|
2125
|
+
successful: false,
|
|
2126
|
+
reason:
|
|
2127
|
+
'parameter list is empty after pre-set callback; set rejected',
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// 2) Validate
|
|
2018
2134
|
let result = this._validateParameters(parameters, declareParameterMode);
|
|
2019
2135
|
if (!result.successful) {
|
|
2020
2136
|
return result;
|
|
@@ -2084,6 +2200,11 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
2084
2200
|
// Publish ParameterEvent.
|
|
2085
2201
|
this._parameterEventPublisher.publish(parameterEvent);
|
|
2086
2202
|
|
|
2203
|
+
// POST callbacks — for side effects after successful set
|
|
2204
|
+
for (const callback of this._postSetParametersCallbacks) {
|
|
2205
|
+
callback(parameters);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2087
2208
|
return {
|
|
2088
2209
|
successful: true,
|
|
2089
2210
|
reason: '',
|
|
@@ -2128,6 +2249,82 @@ class Node extends rclnodejs.ShadowNode {
|
|
|
2128
2249
|
}
|
|
2129
2250
|
}
|
|
2130
2251
|
|
|
2252
|
+
/**
|
|
2253
|
+
* A callback invoked before parameter validation and setting.
|
|
2254
|
+
* It receives the parameter list and must return a (possibly modified) parameter list.
|
|
2255
|
+
*
|
|
2256
|
+
* @callback PreSetParametersCallback
|
|
2257
|
+
* @param {Parameter[]} parameters - The parameters about to be set.
|
|
2258
|
+
* @returns {Parameter[]} - The modified parameter list to proceed with.
|
|
2259
|
+
*
|
|
2260
|
+
* @see [Node.addPreSetParametersCallback]{@link Node#addPreSetParametersCallback}
|
|
2261
|
+
* @see [Node.removePreSetParametersCallback]{@link Node#removePreSetParametersCallback}
|
|
2262
|
+
*/
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* Add a callback invoked before parameter validation.
|
|
2266
|
+
* The callback receives the parameter list and must return a (possibly modified)
|
|
2267
|
+
* parameter list. This can be used to coerce, add, or remove parameters before
|
|
2268
|
+
* they are validated and applied. If any pre-set callback returns an empty list,
|
|
2269
|
+
* the set is rejected.
|
|
2270
|
+
*
|
|
2271
|
+
* @param {PreSetParametersCallback} callback - The callback to add.
|
|
2272
|
+
* @returns {undefined}
|
|
2273
|
+
*/
|
|
2274
|
+
addPreSetParametersCallback(callback) {
|
|
2275
|
+
this._preSetParametersCallbacks.unshift(callback);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Remove a pre-set parameters callback.
|
|
2280
|
+
*
|
|
2281
|
+
* @param {PreSetParametersCallback} callback - The callback to remove.
|
|
2282
|
+
* @returns {undefined}
|
|
2283
|
+
*/
|
|
2284
|
+
removePreSetParametersCallback(callback) {
|
|
2285
|
+
const idx = this._preSetParametersCallbacks.indexOf(callback);
|
|
2286
|
+
if (idx > -1) {
|
|
2287
|
+
this._preSetParametersCallbacks.splice(idx, 1);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* A callback invoked after parameters have been successfully set.
|
|
2293
|
+
* It receives the final parameter list. For side effects only (return value is ignored).
|
|
2294
|
+
*
|
|
2295
|
+
* @callback PostSetParametersCallback
|
|
2296
|
+
* @param {Parameter[]} parameters - The parameters that were set.
|
|
2297
|
+
* @returns {undefined}
|
|
2298
|
+
*
|
|
2299
|
+
* @see [Node.addPostSetParametersCallback]{@link Node#addPostSetParametersCallback}
|
|
2300
|
+
* @see [Node.removePostSetParametersCallback]{@link Node#removePostSetParametersCallback}
|
|
2301
|
+
*/
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* Add a callback invoked after parameters are successfully set.
|
|
2305
|
+
* The callback receives the final parameter list. Useful for triggering
|
|
2306
|
+
* side effects (e.g., reconfiguring a component when a parameter changes).
|
|
2307
|
+
*
|
|
2308
|
+
* @param {PostSetParametersCallback} callback - The callback to add.
|
|
2309
|
+
* @returns {undefined}
|
|
2310
|
+
*/
|
|
2311
|
+
addPostSetParametersCallback(callback) {
|
|
2312
|
+
this._postSetParametersCallbacks.unshift(callback);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
/**
|
|
2316
|
+
* Remove a post-set parameters callback.
|
|
2317
|
+
*
|
|
2318
|
+
* @param {PostSetParametersCallback} callback - The callback to remove.
|
|
2319
|
+
* @returns {undefined}
|
|
2320
|
+
*/
|
|
2321
|
+
removePostSetParametersCallback(callback) {
|
|
2322
|
+
const idx = this._postSetParametersCallbacks.indexOf(callback);
|
|
2323
|
+
if (idx > -1) {
|
|
2324
|
+
this._postSetParametersCallbacks.splice(idx, 1);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2131
2328
|
/**
|
|
2132
2329
|
* Get the fully qualified name of the node.
|
|
2133
2330
|
*
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const { TypeValidationError, OperationError } = require('./errors');
|
|
18
18
|
const { normalizeNodeName } = require('./utils');
|
|
19
|
+
const validator = require('./validator');
|
|
19
20
|
const debug = require('debug')('rclnodejs:parameter_event_handler');
|
|
20
21
|
|
|
21
22
|
const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
|
|
@@ -210,6 +211,64 @@ class ParameterEventHandler {
|
|
|
210
211
|
return handle;
|
|
211
212
|
}
|
|
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
|
+
|
|
213
272
|
/**
|
|
214
273
|
* Remove a previously added parameter callback.
|
|
215
274
|
*
|
|
@@ -450,6 +509,45 @@ class ParameterEventHandler {
|
|
|
450
509
|
return `${paramName}\0${nodeName}`;
|
|
451
510
|
}
|
|
452
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
|
+
|
|
453
551
|
/**
|
|
454
552
|
* Check if the handler has been destroyed and throw if so.
|
|
455
553
|
* @private
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Copyright (c) 2026 The Robot Web Tools Contributors. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const QoS = require('./qos.js');
|
|
18
|
+
const {
|
|
19
|
+
Parameter,
|
|
20
|
+
ParameterType,
|
|
21
|
+
ParameterDescriptor,
|
|
22
|
+
} = require('./parameter.js');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enum of overridable QoS policy kinds.
|
|
26
|
+
* Each value corresponds to a QoS property on the {@link QoS} class.
|
|
27
|
+
* @enum {number}
|
|
28
|
+
*/
|
|
29
|
+
const QoSPolicyKind = Object.freeze({
|
|
30
|
+
HISTORY: 1,
|
|
31
|
+
DEPTH: 2,
|
|
32
|
+
RELIABILITY: 3,
|
|
33
|
+
DURABILITY: 4,
|
|
34
|
+
LIVELINESS: 5,
|
|
35
|
+
AVOID_ROS_NAMESPACE_CONVENTIONS: 6,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Maps QoSPolicyKind -> { qosProp, paramKey, paramType, toParam, fromParam }
|
|
39
|
+
const POLICY_MAP = {
|
|
40
|
+
[QoSPolicyKind.HISTORY]: {
|
|
41
|
+
qosProp: 'history',
|
|
42
|
+
paramKey: 'history',
|
|
43
|
+
enumObj: QoS.HistoryPolicy,
|
|
44
|
+
paramType: ParameterType.PARAMETER_STRING,
|
|
45
|
+
toParam: (val, enumObj) => _enumToString(val, enumObj),
|
|
46
|
+
fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
|
|
47
|
+
},
|
|
48
|
+
[QoSPolicyKind.DEPTH]: {
|
|
49
|
+
qosProp: 'depth',
|
|
50
|
+
paramKey: 'depth',
|
|
51
|
+
paramType: ParameterType.PARAMETER_INTEGER,
|
|
52
|
+
toParam: (val) => BigInt(val),
|
|
53
|
+
fromParam: (val) => Number(val),
|
|
54
|
+
},
|
|
55
|
+
[QoSPolicyKind.RELIABILITY]: {
|
|
56
|
+
qosProp: 'reliability',
|
|
57
|
+
paramKey: 'reliability',
|
|
58
|
+
enumObj: QoS.ReliabilityPolicy,
|
|
59
|
+
paramType: ParameterType.PARAMETER_STRING,
|
|
60
|
+
toParam: (val, enumObj) => _enumToString(val, enumObj),
|
|
61
|
+
fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
|
|
62
|
+
},
|
|
63
|
+
[QoSPolicyKind.DURABILITY]: {
|
|
64
|
+
qosProp: 'durability',
|
|
65
|
+
paramKey: 'durability',
|
|
66
|
+
enumObj: QoS.DurabilityPolicy,
|
|
67
|
+
paramType: ParameterType.PARAMETER_STRING,
|
|
68
|
+
toParam: (val, enumObj) => _enumToString(val, enumObj),
|
|
69
|
+
fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
|
|
70
|
+
},
|
|
71
|
+
[QoSPolicyKind.LIVELINESS]: {
|
|
72
|
+
qosProp: 'liveliness',
|
|
73
|
+
paramKey: 'liveliness',
|
|
74
|
+
enumObj: QoS.LivelinessPolicy,
|
|
75
|
+
paramType: ParameterType.PARAMETER_STRING,
|
|
76
|
+
toParam: (val, enumObj) => _enumToString(val, enumObj),
|
|
77
|
+
fromParam: (val, enumObj) => _stringToEnum(val, enumObj),
|
|
78
|
+
},
|
|
79
|
+
[QoSPolicyKind.AVOID_ROS_NAMESPACE_CONVENTIONS]: {
|
|
80
|
+
qosProp: 'avoidRosNameSpaceConventions',
|
|
81
|
+
paramKey: 'avoid_ros_namespace_conventions',
|
|
82
|
+
paramType: ParameterType.PARAMETER_BOOL,
|
|
83
|
+
toParam: (val) => val,
|
|
84
|
+
fromParam: (val) => Boolean(val),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert a numeric enum value to a lowercase string name.
|
|
90
|
+
* @param {number} val - enum numeric value
|
|
91
|
+
* @param {Object} enumObj - the enum object (e.g. QoS.HistoryPolicy)
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
function _enumToString(val, enumObj) {
|
|
95
|
+
for (const [key, v] of Object.entries(enumObj)) {
|
|
96
|
+
if (v === val) {
|
|
97
|
+
// Strip the RMW prefix: "RMW_QOS_POLICY_HISTORY_KEEP_LAST" -> "keep_last"
|
|
98
|
+
const parts = key.split('_');
|
|
99
|
+
// Find the index after the policy name (HISTORY, RELIABILITY, etc.)
|
|
100
|
+
// Pattern: RMW_QOS_POLICY_<POLICY>_<VALUE>
|
|
101
|
+
const policyNames = [
|
|
102
|
+
'HISTORY',
|
|
103
|
+
'RELIABILITY',
|
|
104
|
+
'DURABILITY',
|
|
105
|
+
'LIVELINESS',
|
|
106
|
+
];
|
|
107
|
+
let valueStart = 4; // default: skip RMW_QOS_POLICY_<X>_
|
|
108
|
+
for (let i = 3; i < parts.length; i++) {
|
|
109
|
+
if (policyNames.includes(parts[i])) {
|
|
110
|
+
valueStart = i + 1;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return parts.slice(valueStart).join('_').toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return String(val);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convert a lowercase string back to a numeric enum value.
|
|
122
|
+
* @param {string} str - the string value (e.g. "keep_last")
|
|
123
|
+
* @param {Object} enumObj - the enum object
|
|
124
|
+
* @returns {number}
|
|
125
|
+
*/
|
|
126
|
+
function _stringToEnum(str, enumObj) {
|
|
127
|
+
const upper = str.toUpperCase();
|
|
128
|
+
for (const [key, val] of Object.entries(enumObj)) {
|
|
129
|
+
if (key.endsWith('_' + upper)) {
|
|
130
|
+
return val;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Unknown QoS policy value: "${str}"`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Options for overriding QoS policies via ROS parameters.
|
|
138
|
+
*
|
|
139
|
+
* When passed to `createPublisher()` or `createSubscription()`, the node
|
|
140
|
+
* will declare read-only parameters for each specified policy kind. These
|
|
141
|
+
* parameters can be set via command-line arguments, launch files, or
|
|
142
|
+
* parameter files to override the QoS profile at startup.
|
|
143
|
+
*
|
|
144
|
+
* Parameter naming convention:
|
|
145
|
+
* `qos_overrides.<topic>.<publisher|subscription>[_<entityId>].<policy>`
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* // Override history, depth, and reliability via parameters
|
|
149
|
+
* const sub = node.createSubscription(
|
|
150
|
+
* 'std_msgs/msg/String', '/chatter',
|
|
151
|
+
* { qos: rclnodejs.QoS.profileDefault,
|
|
152
|
+
* qosOverridingOptions: QoSOverridingOptions.withDefaultPolicies() },
|
|
153
|
+
* (msg) => console.log(msg.data)
|
|
154
|
+
* );
|
|
155
|
+
* // Now you can override via CLI:
|
|
156
|
+
* // --ros-args -p "qos_overrides./chatter.subscription.depth:=20"
|
|
157
|
+
*/
|
|
158
|
+
class QoSOverridingOptions {
|
|
159
|
+
/**
|
|
160
|
+
* @param {Array<QoSPolicyKind>} policyKinds - Which QoS policies to expose as parameters.
|
|
161
|
+
* @param {Object} [opts]
|
|
162
|
+
* @param {function} [opts.callback] - Optional validation callback. Receives the
|
|
163
|
+
* final QoS profile after overrides are applied. Should return
|
|
164
|
+
* `{successful: true}` or `{successful: false, reason: '...'}`.
|
|
165
|
+
* @param {string} [opts.entityId] - Optional suffix to disambiguate multiple
|
|
166
|
+
* publishers/subscriptions on the same topic.
|
|
167
|
+
*/
|
|
168
|
+
constructor(policyKinds, opts = {}) {
|
|
169
|
+
this._policyKinds = Array.from(policyKinds);
|
|
170
|
+
this._callback = opts.callback || null;
|
|
171
|
+
this._entityId = opts.entityId || null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get policyKinds() {
|
|
175
|
+
return this._policyKinds;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get callback() {
|
|
179
|
+
return this._callback;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get entityId() {
|
|
183
|
+
return this._entityId;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create options that override history, depth, and reliability —
|
|
188
|
+
* the most commonly tuned policies.
|
|
189
|
+
* @param {Object} [opts]
|
|
190
|
+
* @param {function} [opts.callback] - Validation callback.
|
|
191
|
+
* @param {string} [opts.entityId] - Entity disambiguation suffix.
|
|
192
|
+
* @returns {QoSOverridingOptions}
|
|
193
|
+
*/
|
|
194
|
+
static withDefaultPolicies(opts = {}) {
|
|
195
|
+
return new QoSOverridingOptions(
|
|
196
|
+
[QoSPolicyKind.HISTORY, QoSPolicyKind.DEPTH, QoSPolicyKind.RELIABILITY],
|
|
197
|
+
opts
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve QoS profile string to a mutable QoS object.
|
|
204
|
+
* If already a QoS instance, return as-is.
|
|
205
|
+
* @param {QoS|string} qos
|
|
206
|
+
* @returns {QoS}
|
|
207
|
+
*/
|
|
208
|
+
function _resolveQoS(qos) {
|
|
209
|
+
if (qos instanceof QoS) {
|
|
210
|
+
return qos;
|
|
211
|
+
}
|
|
212
|
+
// Plain object with QoS fields — construct a QoS from its properties
|
|
213
|
+
if (typeof qos === 'object' && qos !== null && !Array.isArray(qos)) {
|
|
214
|
+
return new QoS(
|
|
215
|
+
qos.history,
|
|
216
|
+
qos.depth,
|
|
217
|
+
qos.reliability,
|
|
218
|
+
qos.durability,
|
|
219
|
+
qos.liveliness,
|
|
220
|
+
qos.avoidRosNameSpaceConventions
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
// Profile string: create a QoS with the corresponding defaults
|
|
224
|
+
// Values must match the rmw_qos_profile_* definitions in rmw/types.h
|
|
225
|
+
switch (qos) {
|
|
226
|
+
case QoS.profileDefault:
|
|
227
|
+
return new QoS(
|
|
228
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
|
229
|
+
10,
|
|
230
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
|
|
231
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
|
|
232
|
+
);
|
|
233
|
+
case QoS.profileSystemDefault:
|
|
234
|
+
return new QoS(
|
|
235
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT,
|
|
236
|
+
0,
|
|
237
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_SYSTEM_DEFAULT,
|
|
238
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_SYSTEM_DEFAULT,
|
|
239
|
+
QoS.LivelinessPolicy.RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT
|
|
240
|
+
);
|
|
241
|
+
case QoS.profileSensorData:
|
|
242
|
+
return new QoS(
|
|
243
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
|
244
|
+
5,
|
|
245
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT,
|
|
246
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
|
|
247
|
+
);
|
|
248
|
+
case QoS.profileServicesDefault:
|
|
249
|
+
return new QoS(
|
|
250
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
|
251
|
+
10,
|
|
252
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
|
|
253
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
|
|
254
|
+
);
|
|
255
|
+
case QoS.profileParameters:
|
|
256
|
+
return new QoS(
|
|
257
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
|
258
|
+
1000,
|
|
259
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
|
|
260
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
|
|
261
|
+
);
|
|
262
|
+
case QoS.profileParameterEvents:
|
|
263
|
+
return new QoS(
|
|
264
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
|
265
|
+
1000,
|
|
266
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
|
|
267
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
|
|
268
|
+
);
|
|
269
|
+
case QoS.profileActionStatusDefault:
|
|
270
|
+
return new QoS(
|
|
271
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
|
272
|
+
1,
|
|
273
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
|
|
274
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL
|
|
275
|
+
);
|
|
276
|
+
default:
|
|
277
|
+
return new QoS(
|
|
278
|
+
QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_KEEP_LAST,
|
|
279
|
+
10,
|
|
280
|
+
QoS.ReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE,
|
|
281
|
+
QoS.DurabilityPolicy.RMW_QOS_POLICY_DURABILITY_VOLATILE
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Declare QoS override parameters on the node and apply any overrides
|
|
288
|
+
* to the QoS profile in-place.
|
|
289
|
+
*
|
|
290
|
+
* @param {'publisher'|'subscription'} entityType
|
|
291
|
+
* @param {Node} node
|
|
292
|
+
* @param {string} topic - Fully resolved topic name.
|
|
293
|
+
* @param {QoS} qos - Mutable QoS object (will be modified in-place).
|
|
294
|
+
* @param {QoSOverridingOptions} options
|
|
295
|
+
*/
|
|
296
|
+
function declareQosParameters(entityType, node, topic, qos, options) {
|
|
297
|
+
if (!options || options.policyKinds.length === 0) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const idSuffix = options.entityId ? `_${options.entityId}` : '';
|
|
302
|
+
const namePrefix = `qos_overrides.${topic}.${entityType}${idSuffix}`;
|
|
303
|
+
|
|
304
|
+
for (const policyKind of options.policyKinds) {
|
|
305
|
+
const mapping = POLICY_MAP[policyKind];
|
|
306
|
+
if (!mapping) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const paramName = `${namePrefix}.${mapping.paramKey}`;
|
|
311
|
+
const currentValue = qos[mapping.qosProp];
|
|
312
|
+
const paramValue = mapping.toParam(currentValue, mapping.enumObj);
|
|
313
|
+
|
|
314
|
+
const descriptor = new ParameterDescriptor(
|
|
315
|
+
paramName,
|
|
316
|
+
mapping.paramType,
|
|
317
|
+
`QoS override for ${mapping.qosProp}`,
|
|
318
|
+
true // readOnly
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
let param;
|
|
322
|
+
try {
|
|
323
|
+
param = node.declareParameter(
|
|
324
|
+
new Parameter(paramName, mapping.paramType, paramValue),
|
|
325
|
+
descriptor
|
|
326
|
+
);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
// Already declared (e.g. multiple entities on same topic) — reuse
|
|
329
|
+
if (node.hasParameter(paramName)) {
|
|
330
|
+
param = node.getParameter(paramName);
|
|
331
|
+
} else {
|
|
332
|
+
throw e;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Apply the (possibly overridden) parameter value back to QoS
|
|
337
|
+
if (param && param.value !== paramValue) {
|
|
338
|
+
qos[mapping.qosProp] = mapping.fromParam(param.value, mapping.enumObj);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Run validation callback if provided
|
|
343
|
+
if (options.callback) {
|
|
344
|
+
const result = options.callback(qos);
|
|
345
|
+
if (result && !result.successful) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`QoS override validation failed: ${result.reason || 'unknown reason'}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = {
|
|
354
|
+
QoSPolicyKind,
|
|
355
|
+
QoSOverridingOptions,
|
|
356
|
+
declareQosParameters,
|
|
357
|
+
_resolveQoS,
|
|
358
|
+
};
|
package/lib/timer.js
CHANGED
|
@@ -150,7 +150,7 @@ class Timer {
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Call a timer and starts counting again, retrieves actual and expected call time.
|
|
153
|
-
* @return {
|
|
153
|
+
* @return {{expectedCallTime: bigint, actualCallTime: bigint}} - The timer information.
|
|
154
154
|
*/
|
|
155
155
|
callTimerWithInfo() {
|
|
156
156
|
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rclnodejs",
|
|
3
|
-
"version": "1.9.0
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "ROS2.0 JavaScript client with Node.js",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"jsdoc": "^4.0.4",
|
|
68
68
|
"lint-staged": "^16.2.0",
|
|
69
69
|
"mocha": "^11.0.2",
|
|
70
|
-
"node-gyp": "^
|
|
70
|
+
"node-gyp": "^10.3.1",
|
|
71
71
|
"nyc": "^18.0.0",
|
|
72
72
|
"prebuildify": "^6.0.1",
|
|
73
73
|
"rimraf": "^6.0.1",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -78,6 +78,15 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
|
|
|
78
78
|
|
|
79
79
|
bool lossless;
|
|
80
80
|
int64_t period_nsec = info[2].As<Napi::BigInt>().Int64Value(&lossless);
|
|
81
|
+
bool autostart = true;
|
|
82
|
+
if (info.Length() > 3) {
|
|
83
|
+
if (!info[3].IsBoolean()) {
|
|
84
|
+
Napi::TypeError::New(env, "Timer autostart must be a boolean")
|
|
85
|
+
.ThrowAsJavaScriptException();
|
|
86
|
+
return env.Undefined();
|
|
87
|
+
}
|
|
88
|
+
autostart = info[3].As<Napi::Boolean>().Value();
|
|
89
|
+
}
|
|
81
90
|
rcl_timer_t* timer =
|
|
82
91
|
reinterpret_cast<rcl_timer_t*>(malloc(sizeof(rcl_timer_t)));
|
|
83
92
|
*timer = rcl_get_zero_initialized_timer();
|
|
@@ -85,8 +94,7 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
|
|
|
85
94
|
#if ROS_VERSION > 2305 // After Iron.
|
|
86
95
|
{
|
|
87
96
|
rcl_ret_t ret = rcl_timer_init2(timer, clock, context, period_nsec, nullptr,
|
|
88
|
-
rcl_get_default_allocator(),
|
|
89
|
-
/*autostart=*/true);
|
|
97
|
+
rcl_get_default_allocator(), autostart);
|
|
90
98
|
if (RCL_RET_OK != ret) {
|
|
91
99
|
std::string error_msg = rcl_get_error_string().str;
|
|
92
100
|
rcl_reset_error();
|
|
@@ -106,6 +114,17 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
|
|
|
106
114
|
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
|
|
107
115
|
return env.Undefined();
|
|
108
116
|
}
|
|
117
|
+
if (!autostart) {
|
|
118
|
+
rcl_ret_t cancel_ret = rcl_timer_cancel(timer);
|
|
119
|
+
if (RCL_RET_OK != cancel_ret) {
|
|
120
|
+
std::string error_msg = rcl_get_error_string().str;
|
|
121
|
+
rcl_reset_error();
|
|
122
|
+
rcl_timer_fini(timer);
|
|
123
|
+
free(timer);
|
|
124
|
+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
|
|
125
|
+
return env.Undefined();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
109
128
|
}
|
|
110
129
|
#endif
|
|
111
130
|
|
package/types/node.d.ts
CHANGED
|
@@ -80,6 +80,13 @@ declare module 'rclnodejs' {
|
|
|
80
80
|
* inspect and limit messages that it accepts.
|
|
81
81
|
*/
|
|
82
82
|
contentFilter?: SubscriptionContentFilter;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* If provided, declares read-only ROS parameters for the specified QoS policies.
|
|
86
|
+
* These can be overridden at startup via `--ros-args -p` or `--params-file`.
|
|
87
|
+
* If qos is a profile string, it will be resolved to a mutable QoS object.
|
|
88
|
+
*/
|
|
89
|
+
qosOverridingOptions?: QoSOverridingOptions;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/**
|
|
@@ -97,14 +104,24 @@ declare module 'rclnodejs' {
|
|
|
97
104
|
*/
|
|
98
105
|
const DEFAULT_OPTIONS: Options;
|
|
99
106
|
|
|
107
|
+
interface TimerInfo {
|
|
108
|
+
expectedCallTime: bigint;
|
|
109
|
+
actualCallTime: bigint;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface TimerOptions {
|
|
113
|
+
autostart?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
/**
|
|
101
117
|
* Callback for receiving periodic interrupts from a Timer.
|
|
118
|
+
* Receives timer metadata when the underlying ROS distro exposes it.
|
|
102
119
|
*
|
|
103
120
|
* @remarks
|
|
104
121
|
* See {@link Node.createTimer | Node.createTimer}
|
|
105
122
|
* See {@link Timer}
|
|
106
123
|
*/
|
|
107
|
-
type TimerRequestCallback = () => void;
|
|
124
|
+
type TimerRequestCallback = (timerInfo?: TimerInfo) => void;
|
|
108
125
|
|
|
109
126
|
/**
|
|
110
127
|
* Callback indicating parameters are about to be declared or set.
|
|
@@ -123,6 +140,19 @@ declare module 'rclnodejs' {
|
|
|
123
140
|
parameters: Parameter[]
|
|
124
141
|
) => rcl_interfaces.msg.SetParametersResult;
|
|
125
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Callback invoked before parameter validation and setting.
|
|
145
|
+
* Receives the parameter list, must return a (possibly modified) parameter list.
|
|
146
|
+
* Returning an empty list rejects the set.
|
|
147
|
+
*/
|
|
148
|
+
type PreSetParametersCallback = (parameters: Parameter[]) => Parameter[];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Callback invoked after parameters have been successfully set.
|
|
152
|
+
* For side effects only (return value is ignored).
|
|
153
|
+
*/
|
|
154
|
+
type PostSetParametersCallback = (parameters: Parameter[]) => void;
|
|
155
|
+
|
|
126
156
|
/**
|
|
127
157
|
* Standard result of Node.getXXXNamesAndTypes() queries
|
|
128
158
|
*
|
|
@@ -293,15 +323,18 @@ declare module 'rclnodejs' {
|
|
|
293
323
|
/**
|
|
294
324
|
* Create a Timer.
|
|
295
325
|
*
|
|
296
|
-
* @param period - Elapsed time between interrupt events
|
|
297
|
-
* @param callback - Called
|
|
298
|
-
* @param
|
|
326
|
+
* @param period - Elapsed time between interrupt events in nanoseconds.
|
|
327
|
+
* @param callback - Called when the timer fires. Receives a `TimerInfo` argument when available.
|
|
328
|
+
* @param optionsOrClock - Optional timer options or clock to use for the timer.
|
|
329
|
+
* Supports `{ autostart?: boolean }` when an options object is provided.
|
|
330
|
+
* @param clock - Optional clock to use for the timer when options are provided.
|
|
299
331
|
* @returns New instance of Timer.
|
|
300
332
|
*/
|
|
301
333
|
createTimer(
|
|
302
334
|
period: bigint,
|
|
303
335
|
callback: TimerRequestCallback,
|
|
304
|
-
|
|
336
|
+
optionsOrClock?: TimerOptions | Clock | null,
|
|
337
|
+
clock?: Clock | null
|
|
305
338
|
): Timer;
|
|
306
339
|
|
|
307
340
|
/**
|
|
@@ -749,6 +782,37 @@ declare module 'rclnodejs' {
|
|
|
749
782
|
*/
|
|
750
783
|
removeOnSetParametersCallback(call: SetParametersCallback): void;
|
|
751
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Add a callback invoked before parameter validation.
|
|
787
|
+
* The callback receives the parameter list and must return a (possibly modified)
|
|
788
|
+
* parameter list. Returning an empty list rejects the set.
|
|
789
|
+
*
|
|
790
|
+
* @param callback - The callback to add.
|
|
791
|
+
*/
|
|
792
|
+
addPreSetParametersCallback(callback: PreSetParametersCallback): void;
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Remove a pre-set parameters callback.
|
|
796
|
+
*
|
|
797
|
+
* @param callback - The callback to remove.
|
|
798
|
+
*/
|
|
799
|
+
removePreSetParametersCallback(callback: PreSetParametersCallback): void;
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Add a callback invoked after parameters are successfully set.
|
|
803
|
+
* Useful for triggering side effects (e.g., reconfiguring a component).
|
|
804
|
+
*
|
|
805
|
+
* @param callback - The callback to add.
|
|
806
|
+
*/
|
|
807
|
+
addPostSetParametersCallback(callback: PostSetParametersCallback): void;
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Remove a post-set parameters callback.
|
|
811
|
+
*
|
|
812
|
+
* @param callback - The callback to remove.
|
|
813
|
+
*/
|
|
814
|
+
removePostSetParametersCallback(callback: PostSetParametersCallback): void;
|
|
815
|
+
|
|
752
816
|
/**
|
|
753
817
|
* Get a remote node's published topics.
|
|
754
818
|
*
|
|
@@ -97,6 +97,17 @@ declare module 'rclnodejs' {
|
|
|
97
97
|
callback: (event: any) => void
|
|
98
98
|
): ParameterEventCallbackHandle;
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Configure which node parameter events will be received.
|
|
102
|
+
*
|
|
103
|
+
* If nodeNames is omitted or empty, the node filter is cleared.
|
|
104
|
+
* Relative names are resolved against the handler node namespace.
|
|
105
|
+
*
|
|
106
|
+
* @param nodeNames - Node names to filter parameter events from.
|
|
107
|
+
* @returns True if the filter is active or was successfully cleared.
|
|
108
|
+
*/
|
|
109
|
+
configureNodesFilter(nodeNames?: string[]): boolean;
|
|
110
|
+
|
|
100
111
|
/**
|
|
101
112
|
* Remove a previously added parameter event callback.
|
|
102
113
|
*
|
package/types/qos.d.ts
CHANGED
|
@@ -134,4 +134,59 @@ declare module 'rclnodejs' {
|
|
|
134
134
|
RMW_QOS_POLICY_LIVELINESS_BEST_AVAILABLE = 5,
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Enum of overridable QoS policy kinds.
|
|
140
|
+
*/
|
|
141
|
+
enum QoSPolicyKind {
|
|
142
|
+
HISTORY = 1,
|
|
143
|
+
DEPTH = 2,
|
|
144
|
+
RELIABILITY = 3,
|
|
145
|
+
DURABILITY = 4,
|
|
146
|
+
LIVELINESS = 5,
|
|
147
|
+
AVOID_ROS_NAMESPACE_CONVENTIONS = 6,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Options for overriding QoS policies via ROS parameters.
|
|
152
|
+
*
|
|
153
|
+
* When passed to `createPublisher()` or `createSubscription()`, the node
|
|
154
|
+
* declares read-only parameters for each specified policy kind. These
|
|
155
|
+
* parameters can be overridden at startup via `--ros-args -p` or `--params-file`.
|
|
156
|
+
*
|
|
157
|
+
* Parameter naming convention:
|
|
158
|
+
* `qos_overrides.<topic>.<publisher|subscription>[_<entityId>].<policy>`
|
|
159
|
+
*/
|
|
160
|
+
class QoSOverridingOptions {
|
|
161
|
+
/**
|
|
162
|
+
* @param policyKinds - Which QoS policies to expose as parameters.
|
|
163
|
+
* @param opts - Optional callback and entityId.
|
|
164
|
+
*/
|
|
165
|
+
constructor(
|
|
166
|
+
policyKinds: QoSPolicyKind[],
|
|
167
|
+
opts?: {
|
|
168
|
+
callback?: (qos: QoS) => { successful: boolean; reason?: string };
|
|
169
|
+
entityId?: string;
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
/** Which QoS policies are exposed as parameters. */
|
|
174
|
+
readonly policyKinds: QoSPolicyKind[];
|
|
175
|
+
|
|
176
|
+
/** Optional validation callback. */
|
|
177
|
+
readonly callback:
|
|
178
|
+
| ((qos: QoS) => { successful: boolean; reason?: string })
|
|
179
|
+
| null;
|
|
180
|
+
|
|
181
|
+
/** Optional entity disambiguation suffix. */
|
|
182
|
+
readonly entityId: string | null;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create options that override history, depth, and reliability.
|
|
186
|
+
*/
|
|
187
|
+
static withDefaultPolicies(opts?: {
|
|
188
|
+
callback?: (qos: QoS) => { successful: boolean; reason?: string };
|
|
189
|
+
entityId?: string;
|
|
190
|
+
}): QoSOverridingOptions;
|
|
191
|
+
}
|
|
137
192
|
}
|
package/types/timer.d.ts
CHANGED
|
@@ -78,8 +78,9 @@ declare module 'rclnodejs' {
|
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Call a timer and starts counting again, retrieves actual and expected call time.
|
|
81
|
-
*
|
|
81
|
+
*
|
|
82
|
+
* @return The timer information with expected and actual call timestamps.
|
|
82
83
|
*/
|
|
83
|
-
callTimerWithInfo():
|
|
84
|
+
callTimerWithInfo(): TimerInfo;
|
|
84
85
|
}
|
|
85
86
|
}
|