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 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
- * Updates the goal handle with the execute status and begins exection.
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
- rclnodejs.callTimer(timer.handle);
254
- timer.callback();
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 timeout.
626
- * @param {Clock} [clock] - The clock which the timer gets time from.
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
- if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
631
- clock = null;
632
- } else if (arguments.length === 4) {
633
- clock = arguments[3];
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 {object} - The timer information.
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-alpha.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": "^12.1.0",
70
+ "node-gyp": "^10.3.1",
71
71
  "nyc": "^18.0.0",
72
72
  "prebuildify": "^6.0.1",
73
73
  "rimraf": "^6.0.1",
@@ -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 (milliseconds).
297
- * @param callback - Called on timeout interrupt.
298
- * @param clock - Optional clock to use for the timer.
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
- clock?: Clock
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
- * @return - The timer information.
81
+ *
82
+ * @return The timer information with expected and actual call timestamps.
82
83
  */
83
- callTimerWithInfo(): object;
84
+ callTimerWithInfo(): TimerInfo;
84
85
  }
85
86
  }