• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobotWebTools / rclnodejs / 15461874779

05 Jun 2025 08:09AM UTC coverage: 84.625% (-0.2%) from 84.823%
15461874779

Pull #1159

github

web-flow
Merge 3991d0e5e into 0dbe3da92
Pull Request #1159: Implement event handler

761 of 989 branches covered (76.95%)

Branch coverage included in aggregate %.

92 of 117 new or added lines in 4 files covered. (78.63%)

25 existing lines in 1 file now uncovered.

1881 of 2133 relevant lines covered (88.19%)

2769.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.99
/lib/node.js
1
// Copyright (c) 2017 Intel Corporation. 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 rclnodejs = require('bindings')('rclnodejs');
208✔
18

19
const ActionInterfaces = require('./action/interfaces.js');
208✔
20
const Client = require('./client.js');
208✔
21
const Clock = require('./clock.js');
208✔
22
const Context = require('./context.js');
208✔
23
const debug = require('debug')('rclnodejs:node');
208✔
24
const DistroUtils = require('./distro.js');
208✔
25
const GuardCondition = require('./guard_condition.js');
208✔
26
const loader = require('./interface_loader.js');
208✔
27
const Logging = require('./logging.js');
208✔
28
const NodeOptions = require('./node_options.js');
208✔
29
const {
30
  ParameterType,
31
  Parameter,
32
  ParameterDescriptor,
33
} = require('./parameter.js');
208✔
34
const ParameterService = require('./parameter_service.js');
208✔
35
const Publisher = require('./publisher.js');
208✔
36
const QoS = require('./qos.js');
208✔
37
const Rates = require('./rate.js');
208✔
38
const Service = require('./service.js');
208✔
39
const Subscription = require('./subscription.js');
208✔
40
const TimeSource = require('./time_source.js');
208✔
41
const Timer = require('./timer.js');
208✔
42
const TypeDescriptionService = require('./type_description_service.js');
208✔
43
const Entity = require('./entity.js');
208✔
44
const { SubscriptionEventCallbacks } = require('../lib/event_handler.js');
208✔
45
const { PublisherEventCallbacks } = require('../lib/event_handler.js');
208✔
46

47
// Parameter event publisher constants
48
const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
208✔
49
const PARAMETER_EVENT_TOPIC = 'parameter_events';
208✔
50

51
/**
52
 * @class - Class representing a Node in ROS
53
 */
54

55
class Node extends rclnodejs.ShadowNode {
56
  /**
57
   * Create a ROS2Node.
58
   * model using the {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}.
59
   * @param {string} nodeName - The name used to register in ROS.
60
   * @param {string} [namespace=''] - The namespace used in ROS.
61
   * @param {Context} [context=Context.defaultContext()] - The context to create the node in.
62
   * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior.
63
   * @throws {Error} If the given context is not registered.
64
   */
65
  constructor(
66
    nodeName,
67
    namespace = '',
234✔
68
    context = Context.defaultContext(),
514✔
69
    options = NodeOptions.defaultOptions
530✔
70
  ) {
71
    super();
4,842✔
72

73
    if (typeof nodeName !== 'string' || typeof namespace !== 'string') {
4,842✔
74
      throw new TypeError('Invalid argument.');
176✔
75
    }
76

77
    this._init(nodeName, namespace, options, context);
4,666✔
78
    debug(
4,610✔
79
      'Finish initializing node, name = %s and namespace = %s.',
80
      nodeName,
81
      namespace
82
    );
83
  }
84

85
  _init(name, namespace, options, context) {
86
    this.handle = rclnodejs.createNode(name, namespace, context.handle);
4,666✔
87
    Object.defineProperty(this, 'handle', {
4,610✔
88
      configurable: false,
89
      writable: false,
90
    }); // make read-only
91

92
    this._context = context;
4,610✔
93
    this.context.onNodeCreated(this);
4,610✔
94

95
    this._publishers = [];
4,610✔
96
    this._subscriptions = [];
4,610✔
97
    this._clients = [];
4,610✔
98
    this._services = [];
4,610✔
99
    this._timers = [];
4,610✔
100
    this._guards = [];
4,610✔
101
    this._events = [];
4,610✔
102
    this._actionClients = [];
4,610✔
103
    this._actionServers = [];
4,610✔
104
    this._rateTimerServer = null;
4,610✔
105
    this._parameterDescriptors = new Map();
4,610✔
106
    this._parameters = new Map();
4,610✔
107
    this._parameterService = null;
4,610✔
108
    this._typeDescriptionService = null;
4,610✔
109
    this._parameterEventPublisher = null;
4,610✔
110
    this._setParametersCallbacks = [];
4,610✔
111
    this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
4,610✔
112
    this._spinning = false;
4,610✔
113

114
    this._parameterEventPublisher = this.createPublisher(
4,610✔
115
      PARAMETER_EVENT_MSG_TYPE,
116
      PARAMETER_EVENT_TOPIC
117
    );
118

119
    // initialize _parameterOverrides from parameters defined on the commandline
120
    this._parameterOverrides = this._getNativeParameterOverrides();
4,610✔
121

122
    // override cli parameterOverrides with those specified in options
123
    if (options.parameterOverrides.length > 0) {
4,610✔
124
      for (const parameter of options.parameterOverrides) {
64✔
125
        if ((!parameter) instanceof Parameter) {
64!
126
          throw new TypeError(
×
127
            'Parameter-override must be an instance of Parameter.'
128
          );
129
        }
130
        this._parameterOverrides.set(parameter.name, parameter);
64✔
131
      }
132
    }
133

134
    // initialize _parameters from parameterOverrides
135
    if (options.automaticallyDeclareParametersFromOverrides) {
4,610✔
136
      for (const parameter of this._parameterOverrides.values()) {
40✔
137
        parameter.validate();
40✔
138
        const descriptor = ParameterDescriptor.fromParameter(parameter);
40✔
139
        this._parameters.set(parameter.name, parameter);
40✔
140
        this._parameterDescriptors.set(parameter.name, descriptor);
40✔
141
      }
142
    }
143

144
    // Clock that has support for ROS time.
145
    // Note: parameter overrides and parameter event publisher need to be ready at this point
146
    // to be able to declare 'use_sim_time' if it was not declared yet.
147
    this._clock = new Clock.ROSClock();
4,610✔
148
    this._timeSource = new TimeSource(this);
4,610✔
149
    this._timeSource.attachClock(this._clock);
4,610✔
150

151
    if (options.startParameterServices) {
4,610✔
152
      this._parameterService = new ParameterService(this);
4,562✔
153
      this._parameterService.start();
4,562✔
154
    }
155

156
    if (
4,610✔
157
      DistroUtils.getDistroId() >= DistroUtils.getDistroId('jazzy') &&
8,090✔
158
      options.startTypeDescriptionService
159
    ) {
160
      this._typeDescriptionService = new TypeDescriptionService(this);
3,480✔
161
      this._typeDescriptionService.start();
3,480✔
162
    }
163
  }
164

165
  execute(handles) {
166
    let timersReady = this._timers.filter((timer) =>
36,186✔
167
      handles.includes(timer.handle)
30,881✔
168
    );
169
    let guardsReady = this._guards.filter((guard) =>
36,186✔
170
      handles.includes(guard.handle)
24✔
171
    );
172
    let subscriptionsReady = this._subscriptions.filter((subscription) =>
36,186✔
173
      handles.includes(subscription.handle)
3,763✔
174
    );
175
    let clientsReady = this._clients.filter((client) =>
36,186✔
176
      handles.includes(client.handle)
743✔
177
    );
178
    let servicesReady = this._services.filter((service) =>
36,186✔
179
      handles.includes(service.handle)
101,813✔
180
    );
181
    let actionClientsReady = this._actionClients.filter((actionClient) =>
36,186✔
182
      handles.includes(actionClient.handle)
1,246✔
183
    );
184
    let actionServersReady = this._actionServers.filter((actionServer) =>
36,186✔
185
      handles.includes(actionServer.handle)
1,246✔
186
    );
187
    let eventsReady = this._events.filter((event) =>
36,186✔
188
      handles.includes(event.handle)
24✔
189
    );
190

191
    timersReady.forEach((timer) => {
36,186✔
192
      if (timer.isReady()) {
30,788✔
193
        rclnodejs.callTimer(timer.handle);
30,352✔
194
        timer.callback();
30,352✔
195
      }
196
    });
197

198
    eventsReady.forEach((event) => {
36,186✔
199
      event.takeData();
24✔
200
    });
201

202
    for (const subscription of subscriptionsReady) {
36,186✔
203
      if (subscription.isDestroyed()) continue;
3,476✔
204
      if (subscription.isRaw) {
3,395✔
205
        let rawMessage = rclnodejs.rclTakeRaw(subscription.handle);
8✔
206
        if (rawMessage) {
8!
207
          subscription.processResponse(rawMessage);
8✔
208
        }
209
        continue;
8✔
210
      }
211

212
      this._runWithMessageType(
3,387✔
213
        subscription.typeClass,
214
        (message, deserialize) => {
215
          let success = rclnodejs.rclTake(subscription.handle, message);
3,387✔
216
          if (success) {
3,387✔
217
            subscription.processResponse(deserialize());
2,816✔
218
          }
219
        }
220
      );
221
    }
222

223
    for (const guard of guardsReady) {
36,186✔
224
      if (guard.isDestroyed()) continue;
24!
225

226
      guard.callback();
24✔
227
    }
228

229
    for (const client of clientsReady) {
36,186✔
230
      if (client.isDestroyed()) continue;
378!
231
      this._runWithMessageType(
378✔
232
        client.typeClass.Response,
233
        (message, deserialize) => {
234
          let sequenceNumber = rclnodejs.rclTakeResponse(
378✔
235
            client.handle,
236
            message
237
          );
238
          if (sequenceNumber !== undefined) {
378✔
239
            client.processResponse(sequenceNumber, deserialize());
232✔
240
          }
241
        }
242
      );
243
    }
244

245
    for (const service of servicesReady) {
36,186✔
246
      if (service.isDestroyed()) continue;
464!
247
      this._runWithMessageType(
464✔
248
        service.typeClass.Request,
249
        (message, deserialize) => {
250
          let header = rclnodejs.rclTakeRequest(
464✔
251
            service.handle,
252
            this.handle,
253
            message
254
          );
255
          if (header) {
464✔
256
            service.processRequest(header, deserialize());
240✔
257
          }
258
        }
259
      );
260
    }
261

262
    for (const actionClient of actionClientsReady) {
36,186✔
263
      if (actionClient.isDestroyed()) continue;
586!
264

265
      const properties = actionClient.handle.properties;
586✔
266

267
      if (properties.isGoalResponseReady) {
586✔
268
        this._runWithMessageType(
289✔
269
          actionClient.typeClass.impl.SendGoalService.Response,
270
          (message, deserialize) => {
271
            let sequence = rclnodejs.actionTakeGoalResponse(
289✔
272
              actionClient.handle,
273
              message
274
            );
275
            if (sequence != undefined) {
289✔
276
              actionClient.processGoalResponse(sequence, deserialize());
240✔
277
            }
278
          }
279
        );
280
      }
281

282
      if (properties.isCancelResponseReady) {
586✔
283
        this._runWithMessageType(
51✔
284
          actionClient.typeClass.impl.CancelGoal.Response,
285
          (message, deserialize) => {
286
            let sequence = rclnodejs.actionTakeCancelResponse(
51✔
287
              actionClient.handle,
288
              message
289
            );
290
            if (sequence != undefined) {
51✔
291
              actionClient.processCancelResponse(sequence, deserialize());
32✔
292
            }
293
          }
294
        );
295
      }
296

297
      if (properties.isResultResponseReady) {
586✔
298
        this._runWithMessageType(
123✔
299
          actionClient.typeClass.impl.GetResultService.Response,
300
          (message, deserialize) => {
301
            let sequence = rclnodejs.actionTakeResultResponse(
123✔
302
              actionClient.handle,
303
              message
304
            );
305
            if (sequence != undefined) {
123✔
306
              actionClient.processResultResponse(sequence, deserialize());
120✔
307
            }
308
          }
309
        );
310
      }
311

312
      if (properties.isFeedbackReady) {
586✔
313
        this._runWithMessageType(
59✔
314
          actionClient.typeClass.impl.FeedbackMessage,
315
          (message, deserialize) => {
316
            let success = rclnodejs.actionTakeFeedback(
59✔
317
              actionClient.handle,
318
              message
319
            );
320
            if (success) {
59✔
321
              actionClient.processFeedbackMessage(deserialize());
40✔
322
            }
323
          }
324
        );
325
      }
326

327
      if (properties.isStatusReady) {
586✔
328
        this._runWithMessageType(
316✔
329
          actionClient.typeClass.impl.GoalStatusArray,
330
          (message, deserialize) => {
331
            let success = rclnodejs.actionTakeStatus(
316✔
332
              actionClient.handle,
333
              message
334
            );
335
            if (success) {
316✔
336
              actionClient.processStatusMessage(deserialize());
266✔
337
            }
338
          }
339
        );
340
      }
341
    }
342

343
    for (const actionServer of actionServersReady) {
36,186✔
344
      if (actionServer.isDestroyed()) continue;
733!
345

346
      const properties = actionServer.handle.properties;
733✔
347

348
      if (properties.isGoalRequestReady) {
733✔
349
        this._runWithMessageType(
258✔
350
          actionServer.typeClass.impl.SendGoalService.Request,
351
          (message, deserialize) => {
352
            const result = rclnodejs.actionTakeGoalRequest(
258✔
353
              actionServer.handle,
354
              message
355
            );
356
            if (result) {
258✔
357
              actionServer.processGoalRequest(result, deserialize());
240✔
358
            }
359
          }
360
        );
361
      }
362

363
      if (properties.isCancelRequestReady) {
733✔
364
        this._runWithMessageType(
55✔
365
          actionServer.typeClass.impl.CancelGoal.Request,
366
          (message, deserialize) => {
367
            const result = rclnodejs.actionTakeCancelRequest(
55✔
368
              actionServer.handle,
369
              message
370
            );
371
            if (result) {
55✔
372
              actionServer.processCancelRequest(result, deserialize());
32✔
373
            }
374
          }
375
        );
376
      }
377

378
      if (properties.isResultRequestReady) {
733✔
379
        this._runWithMessageType(
181✔
380
          actionServer.typeClass.impl.GetResultService.Request,
381
          (message, deserialize) => {
382
            const result = rclnodejs.actionTakeResultRequest(
181✔
383
              actionServer.handle,
384
              message
385
            );
386
            if (result) {
181✔
387
              actionServer.processResultRequest(result, deserialize());
120✔
388
            }
389
          }
390
        );
391
      }
392

393
      if (properties.isGoalExpired) {
733✔
394
        let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
32✔
395
        let message = new GoalInfoArray(actionServer._goalHandles.size);
32✔
396
        let count = rclnodejs.actionExpireGoals(
32✔
397
          actionServer.handle,
398
          actionServer._goalHandles.size,
399
          message._refArray.buffer
400
        );
401
        if (count > 0) {
32✔
402
          actionServer.processGoalExpired(message, count);
24✔
403
        }
404
        GoalInfoArray.freeArray(message);
32✔
405
      }
406
    }
407

408
    // At this point it is safe to clear the cache of any
409
    // destroyed entity references
410
    Entity._gcHandles();
36,186✔
411
  }
412

413
  /**
414
   * Determine if this node is spinning.
415
   * @returns {boolean} - true when spinning; otherwise returns false.
416
   */
417
  get spinning() {
418
    return this._spinning;
32,280✔
419
  }
420

421
  /**
422
   * Trigger the event loop to continuously check for and route.
423
   * incoming events.
424
   * @param {Node} node - The node to be spun up.
425
   * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0.
426
   * @throws {Error} If the node is already spinning.
427
   * @return {undefined}
428
   */
429
  spin(timeout = 10) {
106✔
430
    if (this.spinning) {
3,406!
431
      throw new Error('The node is already spinning.');
×
432
    }
433
    this.start(this.context.handle, timeout);
3,406✔
434
    this._spinning = true;
3,406✔
435
  }
436

437
  /**
438
   * Use spin().
439
   * @deprecated, since 0.18.0
440
   */
441
  startSpinning(timeout) {
442
    this.spin(timeout);
×
443
  }
444

445
  /**
446
   * Terminate spinning - no further events will be received.
447
   * @returns {undefined}
448
   */
449
  stop() {
450
    super.stop();
3,406✔
451
    this._spinning = false;
3,406✔
452
  }
453

454
  /**
455
   * Terminate spinning - no further events will be received.
456
   * @returns {undefined}
457
   * @deprecated since 0.18.0, Use stop().
458
   */
459
  stopSpinning() {
460
    super.stop();
×
461
    this._spinning = false;
×
462
  }
463

464
  /**
465
   * Spin the node and trigger the event loop to check for one incoming event. Thereafter the node
466
   * will not received additional events until running additional calls to spin() or spinOnce().
467
   * @param {Node} node - The node to be spun.
468
   * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0.
469
   * @throws {Error} If the node is already spinning.
470
   * @return {undefined}
471
   */
472
  spinOnce(timeout = 10) {
16✔
473
    if (this.spinning) {
24,072✔
474
      throw new Error('The node is already spinning.');
16✔
475
    }
476
    super.spinOnce(this.context.handle, timeout);
24,056✔
477
  }
478

479
  _removeEntityFromArray(entity, array) {
480
    let index = array.indexOf(entity);
929✔
481
    if (index > -1) {
929✔
482
      array.splice(index, 1);
913✔
483
    }
484
  }
485

486
  _destroyEntity(entity, array, syncHandles = true) {
939✔
487
    if (entity['isDestroyed'] && entity.isDestroyed()) return;
993✔
488

489
    this._removeEntityFromArray(entity, array);
929✔
490
    if (syncHandles) {
929✔
491
      this.syncHandles();
891✔
492
    }
493

494
    if (entity['_destroy']) {
929✔
495
      entity._destroy();
881✔
496
    } else {
497
      // guards and timers
498
      entity.handle.release();
48✔
499
    }
500
  }
501

502
  _validateOptions(options) {
503
    if (
42,891✔
504
      options !== undefined &&
43,275✔
505
      (options === null || typeof options !== 'object')
506
    ) {
507
      throw new TypeError('Invalid argument of options');
160✔
508
    }
509

510
    if (options === undefined) {
42,731✔
511
      return Node.getDefaultOptions();
42,619✔
512
    }
513

514
    if (options.enableTypedArray === undefined) {
112✔
515
      options = Object.assign(options, { enableTypedArray: true });
16✔
516
    }
517

518
    if (options.qos === undefined) {
112✔
519
      options = Object.assign(options, { qos: QoS.profileDefault });
32✔
520
    }
521

522
    if (options.isRaw === undefined) {
112✔
523
      options = Object.assign(options, { isRaw: false });
32✔
524
    }
525

526
    return options;
112✔
527
  }
528

529
  /**
530
   * Create a Timer.
531
   * @param {bigint} period - The number representing period in nanoseconds.
532
   * @param {function} callback - The callback to be called when timeout.
533
   * @param {Clock} [clock] - The clock which the timer gets time from.
534
   * @return {Timer} - An instance of Timer.
535
   */
536
  createTimer(period, callback, clock = null) {
462✔
537
    if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
462!
538
      clock = null;
×
539
    } else if (arguments.length === 4) {
462!
540
      clock = arguments[3];
×
541
    }
542

543
    if (typeof period !== 'bigint' || typeof callback !== 'function') {
462✔
544
      throw new TypeError('Invalid argument');
16✔
545
    }
546

547
    const timerClock = clock || this._clock;
446✔
548
    let timerHandle = rclnodejs.createTimer(
446✔
549
      timerClock.handle,
550
      this.context.handle,
551
      period
552
    );
553
    let timer = new Timer(timerHandle, period, callback);
446✔
554
    debug('Finish creating timer, period = %d.', period);
446✔
555
    this._timers.push(timer);
446✔
556
    this.syncHandles();
446✔
557

558
    return timer;
446✔
559
  }
560

561
  /**
562
   * Create a Rate.
563
   *
564
   * @param {number} hz - The frequency of the rate timer; default is 1 hz.
565
   * @returns {Promise<Rate>} - Promise resolving to new instance of Rate.
566
   */
567
  async createRate(hz = 1) {
32✔
568
    if (typeof hz !== 'number') {
72!
569
      throw new TypeError('Invalid argument');
×
570
    }
571

572
    const MAX_RATE_HZ_IN_MILLISECOND = 1000.0;
72✔
573
    if (hz <= 0.0 || hz > MAX_RATE_HZ_IN_MILLISECOND) {
72✔
574
      throw new RangeError(
16✔
575
        `Hz must be between 0.0 and ${MAX_RATE_HZ_IN_MILLISECOND}`
576
      );
577
    }
578

579
    // lazy initialize rateTimerServer
580
    if (!this._rateTimerServer) {
56✔
581
      this._rateTimerServer = new Rates.RateTimerServer(this);
40✔
582
      await this._rateTimerServer.init();
40✔
583
    }
584

585
    const period = Math.round(1000 / hz);
56✔
586
    const timer = this._rateTimerServer.createTimer(BigInt(period) * 1000000n);
56✔
587
    const rate = new Rates.Rate(hz, timer);
56✔
588

589
    return rate;
56✔
590
  }
591

592
  /**
593
   * Create a Publisher.
594
   * @param {function|string|object} typeClass - The ROS message class,
595
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
596
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
597
   * @param {string} topic - The name of the topic.
598
   * @param {object} options - The options argument used to parameterize the publisher.
599
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
600
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the publisher, default: QoS.profileDefault.
601
   * @return {Publisher} - An instance of Publisher.
602
   */
603
  createPublisher(typeClass, topic, options, eventCallbacks) {
604
    return this._createPublisher(
7,482✔
605
      typeClass,
606
      topic,
607
      options,
608
      Publisher,
609
      eventCallbacks
610
    );
611
  }
612

613
  _createPublisher(typeClass, topic, options, publisherClass, eventCallbacks) {
614
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
7,506✔
615
      typeClass = loader.loadInterface(typeClass);
7,314✔
616
    }
617
    options = this._validateOptions(options);
7,450✔
618

619
    if (
7,450✔
620
      typeof typeClass !== 'function' ||
22,138✔
621
      typeof topic !== 'string' ||
622
      (eventCallbacks && !(eventCallbacks instanceof PublisherEventCallbacks))
623
    ) {
624
      throw new TypeError('Invalid argument');
160✔
625
    }
626

627
    let publisher = publisherClass.createPublisher(
7,290✔
628
      this,
629
      typeClass,
630
      topic,
631
      options,
632
      eventCallbacks
633
    );
634
    debug('Finish creating publisher, topic = %s.', topic);
7,210✔
635
    this._publishers.push(publisher);
7,210✔
636
    return publisher;
7,210✔
637
  }
638

639
  /**
640
   * This callback is called when a message is published
641
   * @callback SubscriptionCallback
642
   * @param {Object} message - The message published
643
   * @see [Node.createSubscription]{@link Node#createSubscription}
644
   * @see [Node.createPublisher]{@link Node#createPublisher}
645
   * @see {@link Publisher}
646
   * @see {@link Subscription}
647
   */
648

649
  /**
650
   * Create a Subscription with optional content-filtering.
651
   * @param {function|string|object} typeClass - The ROS message class,
652
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
653
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
654
   * @param {string} topic - The name of the topic.
655
   * @param {object} options - The options argument used to parameterize the subscription.
656
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
657
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
658
   * @param {boolean} options.isRaw - The topic is serialized when true, default: false.
659
   * @param {object} [options.contentFilter=undefined] - The content-filter, default: undefined.
660
   *  Confirm that your RMW supports content-filtered topics before use. 
661
   * @param {string} options.contentFilter.expression - Specifies the criteria to select the data samples of
662
   *  interest. It is similar to the WHERE part of an SQL clause.
663
   * @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
664
   *  the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
665
   *  fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
666
   * @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.
667
   * @param {SubscriptionEventCallbacks} eventCallbacks - The event callbacks for the subscription.
668
   * @return {Subscription} - An instance of Subscription.
669
   * @throws {ERROR} - May throw an RMW error if content-filter is malformed. 
670
   * @see {@link SubscriptionCallback}
671
   * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B}
672
   */
673
  createSubscription(typeClass, topic, options, callback, eventCallbacks) {
674
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
3,017✔
675
      typeClass = loader.loadInterface(typeClass);
2,945✔
676
    }
677

678
    if (typeof options === 'function') {
2,961✔
679
      callback = options;
2,683✔
680
      options = undefined;
2,683✔
681
    }
682
    options = this._validateOptions(options);
2,961✔
683

684
    if (
2,881✔
685
      typeof typeClass !== 'function' ||
11,356✔
686
      typeof topic !== 'string' ||
687
      typeof callback !== 'function' ||
688
      (eventCallbacks &&
689
        !(eventCallbacks instanceof SubscriptionEventCallbacks))
690
    ) {
691
      throw new TypeError('Invalid argument');
80✔
692
    }
693

694
    let subscription = Subscription.createSubscription(
2,801✔
695
      this,
696
      typeClass,
697
      topic,
698
      options,
699
      callback,
700
      eventCallbacks
701
    );
702
    debug('Finish creating subscription, topic = %s.', topic);
2,713✔
703
    this._subscriptions.push(subscription);
2,713✔
704
    this.syncHandles();
2,713✔
705

706
    return subscription;
2,713✔
707
  }
708

709
  /**
710
   * Create a Client.
711
   * @param {function|string|object} typeClass - The ROS message class,
712
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
713
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
714
   * @param {string} serviceName - The service name to request.
715
   * @param {object} options - The options argument used to parameterize the client.
716
   * @param {boolean} options.enableTypedArray - The response will use TypedArray if necessary, default: true.
717
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the client, default: QoS.profileDefault.
718
   * @return {Client} - An instance of Client.
719
   */
720
  createClient(typeClass, serviceName, options) {
721
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
652✔
722
      typeClass = loader.loadInterface(typeClass);
572✔
723
    }
724
    options = this._validateOptions(options);
596✔
725

726
    if (typeof typeClass !== 'function' || typeof serviceName !== 'string') {
596✔
727
      throw new TypeError('Invalid argument');
160✔
728
    }
729

730
    let client = Client.createClient(
436✔
731
      this.handle,
732
      serviceName,
733
      typeClass,
734
      options
735
    );
736
    debug('Finish creating client, service = %s.', serviceName);
356✔
737
    this._clients.push(client);
356✔
738
    this.syncHandles();
356✔
739

740
    return client;
356✔
741
  }
742

743
  /**
744
   * This callback is called when a request is sent to service
745
   * @callback RequestCallback
746
   * @param {Object} request - The request sent to the service
747
   * @param {Response} response - The response to client.
748
        Use [response.send()]{@link Response#send} to send response object to client
749
   * @return {undefined}
750
   * @see [Node.createService]{@link Node#createService}
751
   * @see [Client.sendRequest]{@link Client#sendRequest}
752
   * @see {@link Client}
753
   * @see {@link Service}
754
   * @see {@link Response#send}
755
   */
756

757
  /**
758
   * Create a Service.
759
   * @param {function|string|object} typeClass - The ROS message class,
760
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
761
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
762
   * @param {string} serviceName - The service name to offer.
763
   * @param {object} options - The options argument used to parameterize the service.
764
   * @param {boolean} options.enableTypedArray - The request will use TypedArray if necessary, default: true.
765
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the service, default: QoS.profileDefault.
766
   * @param {RequestCallback} callback - The callback to be called when receiving request.
767
   * @return {Service} - An instance of Service.
768
   * @see {@link RequestCallback}
769
   */
770
  createService(typeClass, serviceName, options, callback) {
771
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
27,916✔
772
      typeClass = loader.loadInterface(typeClass);
27,836✔
773
    }
774

775
    if (typeof options === 'function') {
27,860✔
776
      callback = options;
27,684✔
777
      options = undefined;
27,684✔
778
    }
779
    options = this._validateOptions(options);
27,860✔
780

781
    if (
27,780✔
782
      typeof typeClass !== 'function' ||
83,228✔
783
      typeof serviceName !== 'string' ||
784
      typeof callback !== 'function'
785
    ) {
786
      throw new TypeError('Invalid argument');
80✔
787
    }
788

789
    let service = Service.createService(
27,700✔
790
      this.handle,
791
      serviceName,
792
      typeClass,
793
      options,
794
      callback
795
    );
796
    debug('Finish creating service, service = %s.', serviceName);
27,620✔
797
    this._services.push(service);
27,620✔
798
    this.syncHandles();
27,620✔
799

800
    return service;
27,620✔
801
  }
802

803
  /**
804
   * Create a guard condition.
805
   * @param {Function} callback - The callback to be called when the guard condition is triggered.
806
   * @return {GuardCondition} - An instance of GuardCondition.
807
   */
808
  createGuardCondition(callback) {
809
    if (typeof callback !== 'function') {
24!
UNCOV
810
      throw new TypeError('Invalid argument');
×
811
    }
812

813
    let guard = GuardCondition.createGuardCondition(callback, this.context);
24✔
814
    debug('Finish creating guard condition');
24✔
815
    this._guards.push(guard);
24✔
816
    this.syncHandles();
24✔
817

818
    return guard;
24✔
819
  }
820

821
  /**
822
   * Destroy all resource allocated by this node, including
823
   * <code>Timer</code>s/<code>Publisher</code>s/<code>Subscription</code>s
824
   * /<code>Client</code>s/<code>Service</code>s
825
   * @return {undefined}
826
   */
827
  destroy() {
828
    if (this.spinning) {
4,786✔
829
      this.stop();
3,386✔
830
    }
831

832
    // Action servers/clients require manual destruction due to circular reference with goal handles.
833
    this._actionClients.forEach((actionClient) => actionClient.destroy());
4,786✔
834
    this._actionServers.forEach((actionServer) => actionServer.destroy());
4,786✔
835

836
    this.context.onNodeDestroyed(this);
4,786✔
837

838
    this.handle.release();
4,786✔
839
    this._clock = null;
4,786✔
840
    this._timers = [];
4,786✔
841
    this._publishers = [];
4,786✔
842
    this._subscriptions = [];
4,786✔
843
    this._clients = [];
4,786✔
844
    this._services = [];
4,786✔
845
    this._guards = [];
4,786✔
846
    this._actionClients = [];
4,786✔
847
    this._actionServers = [];
4,786✔
848

849
    if (this._rateTimerServer) {
4,786✔
850
      this._rateTimerServer.shutdown();
40✔
851
      this._rateTimerServer = null;
40✔
852
    }
853
  }
854

855
  /**
856
   * Destroy a Publisher.
857
   * @param {Publisher} publisher - The Publisher to be destroyed.
858
   * @return {undefined}
859
   */
860
  destroyPublisher(publisher) {
861
    if (!(publisher instanceof Publisher)) {
70✔
862
      throw new TypeError('Invalid argument');
16✔
863
    }
864
    if (publisher.events) {
54!
NEW
865
      publisher.events.forEach((event) => {
×
NEW
866
        this._destroyEntity(event, this._events);
×
867
      });
NEW
868
      publisher.events = [];
×
869
    }
870
    this._destroyEntity(publisher, this._publishers, false);
54✔
871
  }
872

873
  /**
874
   * Destroy a Subscription.
875
   * @param {Subscription} subscription - The Subscription to be destroyed.
876
   * @return {undefined}
877
   */
878
  destroySubscription(subscription) {
879
    if (!(subscription instanceof Subscription)) {
181✔
880
      throw new TypeError('Invalid argument');
16✔
881
    }
882
    if (subscription.events) {
165✔
883
      subscription.events.forEach((event) => {
6✔
884
        this._destroyEntity(event, this._events);
6✔
885
      });
886
      subscription.events = [];
6✔
887
    }
888

889
    this._destroyEntity(subscription, this._subscriptions);
165✔
890
  }
891

892
  /**
893
   * Destroy a Client.
894
   * @param {Client} client - The Client to be destroyed.
895
   * @return {undefined}
896
   */
897
  destroyClient(client) {
898
    if (!(client instanceof Client)) {
64✔
899
      throw new TypeError('Invalid argument');
16✔
900
    }
901
    this._destroyEntity(client, this._clients);
48✔
902
  }
903

904
  /**
905
   * Destroy a Service.
906
   * @param {Service} service - The Service to be destroyed.
907
   * @return {undefined}
908
   */
909
  destroyService(service) {
910
    if (!(service instanceof Service)) {
64✔
911
      throw new TypeError('Invalid argument');
16✔
912
    }
913
    this._destroyEntity(service, this._services);
48✔
914
  }
915

916
  /**
917
   * Destroy a Timer.
918
   * @param {Timer} timer - The Timer to be destroyed.
919
   * @return {undefined}
920
   */
921
  destroyTimer(timer) {
922
    if (!(timer instanceof Timer)) {
64✔
923
      throw new TypeError('Invalid argument');
16✔
924
    }
925
    this._destroyEntity(timer, this._timers);
48✔
926
  }
927

928
  /**
929
   * Destroy a guard condition.
930
   * @param {GuardCondition} guard - The guard condition to be destroyed.
931
   * @return {undefined}
932
   */
933
  destroyGuardCondition(guard) {
934
    if (!(guard instanceof GuardCondition)) {
24!
UNCOV
935
      throw new TypeError('Invalid argument');
×
936
    }
937
    this._destroyEntity(guard, this._guards);
24✔
938
  }
939

940
  /**
941
   * Get the name of the node.
942
   * @return {string}
943
   */
944
  name() {
945
    return rclnodejs.getNodeName(this.handle);
17,766✔
946
  }
947

948
  /**
949
   * Get the namespace of the node.
950
   * @return {string}
951
   */
952
  namespace() {
953
    return rclnodejs.getNamespace(this.handle);
10,076✔
954
  }
955

956
  /**
957
   * Get the context in which this node was created.
958
   * @return {Context}
959
   */
960
  get context() {
961
    return this._context;
41,938✔
962
  }
963

964
  /**
965
   * Get the nodes logger.
966
   * @returns {Logger} - The logger for the node.
967
   */
968
  getLogger() {
969
    return this._logger;
1,128✔
970
  }
971

972
  /**
973
   * Get the clock used by the node.
974
   * @returns {Clock} - The nodes clock.
975
   */
976
  getClock() {
977
    return this._clock;
656✔
978
  }
979

980
  /**
981
   * Get the current time using the node's clock.
982
   * @returns {Time} - The current time.
983
   */
984
  now() {
985
    return this.getClock().now();
16✔
986
  }
987

988
  /**
989
   * Get the list of published topics discovered by the provided node for the remote node name.
990
   * @param {string} nodeName - The name of the node.
991
   * @param {string} namespace - The name of the namespace.
992
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
993
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
994
   */
995
  getPublisherNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
×
UNCOV
996
    return rclnodejs.getPublisherNamesAndTypesByNode(
×
997
      this.handle,
998
      nodeName,
999
      namespace,
1000
      noDemangle
1001
    );
1002
  }
1003

1004
  /**
1005
   * Get the list of published topics discovered by the provided node for the remote node name.
1006
   * @param {string} nodeName - The name of the node.
1007
   * @param {string} namespace - The name of the namespace.
1008
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1009
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1010
   */
1011
  getSubscriptionNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
×
UNCOV
1012
    return rclnodejs.getSubscriptionNamesAndTypesByNode(
×
1013
      this.handle,
1014
      nodeName,
1015
      namespace,
1016
      noDemangle
1017
    );
1018
  }
1019

1020
  /**
1021
   * Get service names and types for which a remote node has servers.
1022
   * @param {string} nodeName - The name of the node.
1023
   * @param {string} namespace - The name of the namespace.
1024
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1025
   */
1026
  getServiceNamesAndTypesByNode(nodeName, namespace) {
UNCOV
1027
    return rclnodejs.getServiceNamesAndTypesByNode(
×
1028
      this.handle,
1029
      nodeName,
1030
      namespace
1031
    );
1032
  }
1033

1034
  /**
1035
   * Get service names and types for which a remote node has clients.
1036
   * @param {string} nodeName - The name of the node.
1037
   * @param {string} namespace - The name of the namespace.
1038
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1039
   */
1040
  getClientNamesAndTypesByNode(nodeName, namespace) {
UNCOV
1041
    return rclnodejs.getClientNamesAndTypesByNode(
×
1042
      this.handle,
1043
      nodeName,
1044
      namespace
1045
    );
1046
  }
1047

1048
  /**
1049
   * Get the list of topics discovered by the provided node.
1050
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1051
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1052
   */
1053
  getTopicNamesAndTypes(noDemangle = false) {
×
UNCOV
1054
    return rclnodejs.getTopicNamesAndTypes(this.handle, noDemangle);
×
1055
  }
1056

1057
  /**
1058
   * Get the list of services discovered by the provided node.
1059
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1060
   */
1061
  getServiceNamesAndTypes() {
1062
    return rclnodejs.getServiceNamesAndTypes(this.handle);
16✔
1063
  }
1064

1065
  /**
1066
   * Get a list of publishers on a given topic.
1067
   * @param {string} topic - the topic name to get the publishers for.
1068
   * @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name,
1069
   *       otherwise it should be a valid ROS topic name.
1070
   * @returns {Array} - list of publishers
1071
   */
1072
  getPublishersInfoByTopic(topic, noDemangle) {
1073
    return rclnodejs.getPublishersInfoByTopic(this.handle, topic, noDemangle);
14✔
1074
  }
1075

1076
  /**
1077
   * Get a list of subscriptions on a given topic.
1078
   * @param {string} topic - the topic name to get the subscriptions for.
1079
   * @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name,
1080
   *       otherwise it should be a valid ROS topic name.
1081
   * @returns {Array} - list of subscriptions
1082
   */
1083
  getSubscriptionsInfoByTopic(topic, noDemangle) {
1084
    return rclnodejs.getSubscriptionsInfoByTopic(
8✔
1085
      this.handle,
1086
      topic,
1087
      noDemangle
1088
    );
1089
  }
1090

1091
  /**
1092
   * Get the list of nodes discovered by the provided node.
1093
   * @return {Array<string>} - An array of the names.
1094
   */
1095
  getNodeNames() {
1096
    return this.getNodeNamesAndNamespaces().map((item) => item.name);
320✔
1097
  }
1098

1099
  /**
1100
   * Get the list of nodes and their namespaces discovered by the provided node.
1101
   * @return {Array<{name: string, namespace: string}>} An array of the names and namespaces.
1102
   */
1103
  getNodeNamesAndNamespaces() {
1104
    return rclnodejs.getNodeNames(this.handle, /*getEnclaves=*/ false);
136✔
1105
  }
1106

1107
  /**
1108
   * Get the list of nodes and their namespaces with enclaves discovered by the provided node.
1109
   * @return {Array<{name: string, namespace: string, enclave: string}>} An array of the names, namespaces and enclaves.
1110
   */
1111
  getNodeNamesAndNamespacesWithEnclaves() {
1112
    return rclnodejs.getNodeNames(this.handle, /*getEnclaves=*/ true);
8✔
1113
  }
1114

1115
  /**
1116
   * Return the number of publishers on a given topic.
1117
   * @param {string} topic - The name of the topic.
1118
   * @returns {number} - Number of publishers on the given topic.
1119
   */
1120
  countPublishers(topic) {
1121
    let expandedTopic = rclnodejs.expandTopicName(
48✔
1122
      topic,
1123
      this.name(),
1124
      this.namespace()
1125
    );
1126
    rclnodejs.validateTopicName(expandedTopic);
48✔
1127

1128
    return rclnodejs.countPublishers(this.handle, expandedTopic);
48✔
1129
  }
1130

1131
  /**
1132
   * Return the number of subscribers on a given topic.
1133
   * @param {string} topic - The name of the topic.
1134
   * @returns {number} - Number of subscribers on the given topic.
1135
   */
1136
  countSubscribers(topic) {
1137
    let expandedTopic = rclnodejs.expandTopicName(
48✔
1138
      topic,
1139
      this.name(),
1140
      this.namespace()
1141
    );
1142
    rclnodejs.validateTopicName(expandedTopic);
48✔
1143

1144
    return rclnodejs.countSubscribers(this.handle, expandedTopic);
48✔
1145
  }
1146

1147
  /**
1148
   * Get the number of clients on a given service name.
1149
   * @param {string} serviceName - the service name
1150
   * @returns {Number}
1151
   */
1152
  countClients(serviceName) {
1153
    if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
12!
UNCOV
1154
      console.warn('countClients is not supported by this version of ROS 2');
×
1155
      return null;
×
1156
    }
1157
    return rclnodejs.countClients(this.handle, serviceName);
12✔
1158
  }
1159

1160
  /**
1161
   * Get the number of services on a given service name.
1162
   * @param {string} serviceName - the service name
1163
   * @returns {Number}
1164
   */
1165
  countServices(serviceName) {
1166
    if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
6!
UNCOV
1167
      console.warn('countServices is not supported by this version of ROS 2');
×
1168
      return null;
×
1169
    }
1170
    return rclnodejs.countServices(this.handle, serviceName);
6✔
1171
  }
1172

1173
  /**
1174
   * Get the list of parameter-overrides found on the commandline and
1175
   * in the NodeOptions.parameter_overrides property.
1176
   *
1177
   * @return {Array<Parameter>} - An array of Parameters.
1178
   */
1179
  getParameterOverrides() {
1180
    return Array.from(this._parameterOverrides.values());
64✔
1181
  }
1182

1183
  /**
1184
   * Declare a parameter.
1185
   *
1186
   * Internally, register a parameter and it's descriptor.
1187
   * If a parameter-override exists, it's value will replace that of the parameter
1188
   * unless ignoreOverride is true.
1189
   * If the descriptor is undefined, then a ParameterDescriptor will be inferred
1190
   * from the parameter's state.
1191
   *
1192
   * If a parameter by the same name has already been declared then an Error is thrown.
1193
   * A parameter must be undeclared before attempting to redeclare it.
1194
   *
1195
   * @param {Parameter} parameter - Parameter to declare.
1196
   * @param {ParameterDescriptor} [descriptor] - Optional descriptor for parameter.
1197
   * @param {boolean} [ignoreOveride] - When true disregard any parameter-override that may be present.
1198
   * @return {Parameter} - The newly declared parameter.
1199
   */
1200
  declareParameter(parameter, descriptor, ignoreOveride = false) {
4,882✔
1201
    const parameters = this.declareParameters(
4,890✔
1202
      [parameter],
1203
      descriptor ? [descriptor] : [],
4,890✔
1204
      ignoreOveride
1205
    );
1206
    return parameters.length == 1 ? parameters[0] : null;
4,890!
1207
  }
1208

1209
  /**
1210
   * Declare a list of parameters.
1211
   *
1212
   * Internally register parameters with their corresponding descriptor one by one
1213
   * in the order they are provided. This is an atomic operation. If an error
1214
   * occurs the process halts and no further parameters are declared.
1215
   * Parameters that have already been processed are undeclared.
1216
   *
1217
   * While descriptors is an optional parameter, when provided there must be
1218
   * a descriptor for each parameter; otherwise an Error is thrown.
1219
   * If descriptors is not provided then a descriptor will be inferred
1220
   * from each parameter's state.
1221
   *
1222
   * When a parameter-override is available, the parameter's value
1223
   * will be replaced with that of the parameter-override unless ignoreOverrides
1224
   * is true.
1225
   *
1226
   * If a parameter by the same name has already been declared then an Error is thrown.
1227
   * A parameter must be undeclared before attempting to redeclare it.
1228
   *
1229
   * Prior to declaring the parameters each SetParameterEventCallback registered
1230
   * using setOnParameterEventCallback() is called in succession with the parameters
1231
   * list. Any SetParameterEventCallback that retuns does not return a successful
1232
   * result will cause the entire operation to terminate with no changes to the
1233
   * parameters. When all SetParameterEventCallbacks return successful then the
1234
   * list of parameters is updated.
1235
   *
1236
   * @param {Parameter[]} parameters - The parameters to declare.
1237
   * @param {ParameterDescriptor[]} [descriptors] - Optional descriptors,
1238
   *    a 1-1 correspondence with parameters.
1239
   * @param {boolean} ignoreOverrides - When true, parameter-overrides are
1240
   *    not considered, i.e.,ignored.
1241
   * @return {Parameter[]} - The declared parameters.
1242
   */
1243
  declareParameters(parameters, descriptors = [], ignoreOverrides = false) {
×
1244
    if (!Array.isArray(parameters)) {
4,890!
UNCOV
1245
      throw new TypeError('Invalid parameter: expected array of Parameter');
×
1246
    }
1247
    if (!Array.isArray(descriptors)) {
4,890!
UNCOV
1248
      throw new TypeError(
×
1249
        'Invalid parameters: expected array of ParameterDescriptor'
1250
      );
1251
    }
1252
    if (descriptors.length > 0 && parameters.length !== descriptors.length) {
4,890!
UNCOV
1253
      throw new TypeError(
×
1254
        'Each parameter must have a cooresponding ParameterDescriptor'
1255
      );
1256
    }
1257

1258
    const declaredDescriptors = [];
4,890✔
1259
    const declaredParameters = [];
4,890✔
1260
    const declaredParameterCollisions = [];
4,890✔
1261
    for (let i = 0; i < parameters.length; i++) {
4,890✔
1262
      let parameter =
1263
        !ignoreOverrides && this._parameterOverrides.has(parameters[i].name)
4,890✔
1264
          ? this._parameterOverrides.get(parameters[i].name)
1265
          : parameters[i];
1266

1267
      // stop processing parameters that have already been declared
1268
      if (this._parameters.has(parameter.name)) {
4,890!
UNCOV
1269
        declaredParameterCollisions.push(parameter);
×
1270
        continue;
×
1271
      }
1272

1273
      // create descriptor for parameter if not provided
1274
      let descriptor =
1275
        descriptors.length > 0
4,890✔
1276
          ? descriptors[i]
1277
          : ParameterDescriptor.fromParameter(parameter);
1278

1279
      descriptor.validate();
4,890✔
1280

1281
      declaredDescriptors.push(descriptor);
4,890✔
1282
      declaredParameters.push(parameter);
4,890✔
1283
    }
1284

1285
    if (declaredParameterCollisions.length > 0) {
4,890!
1286
      const errorMsg =
UNCOV
1287
        declaredParameterCollisions.length == 1
×
1288
          ? `Parameter(${declaredParameterCollisions[0]}) already declared.`
1289
          : `Multiple parameters already declared, e.g., Parameter(${declaredParameterCollisions[0]}).`;
UNCOV
1290
      throw new Error(errorMsg);
×
1291
    }
1292

1293
    // register descriptor
1294
    for (const descriptor of declaredDescriptors) {
4,890✔
1295
      this._parameterDescriptors.set(descriptor.name, descriptor);
4,890✔
1296
    }
1297

1298
    const result = this._setParametersAtomically(declaredParameters, true);
4,890✔
1299
    if (!result.successful) {
4,890!
1300
      // unregister descriptors
UNCOV
1301
      for (const descriptor of declaredDescriptors) {
×
1302
        this._parameterDescriptors.delete(descriptor.name);
×
1303
      }
1304

UNCOV
1305
      throw new Error(result.reason);
×
1306
    }
1307

1308
    return this.getParameters(declaredParameters.map((param) => param.name));
4,890✔
1309
  }
1310

1311
  /**
1312
   * Undeclare a parameter.
1313
   *
1314
   * Readonly parameters can not be undeclared or updated.
1315
   * @param {string} name - Name of parameter to undeclare.
1316
   * @return {undefined} -
1317
   */
1318
  undeclareParameter(name) {
1319
    if (!this.hasParameter(name)) return;
8!
1320

1321
    const descriptor = this.getParameterDescriptor(name);
8✔
1322
    if (descriptor.readOnly) {
8!
UNCOV
1323
      throw new Error(
×
1324
        `${name} parameter is read-only and can not be undeclared`
1325
      );
1326
    }
1327

1328
    this._parameters.delete(name);
8✔
1329
    this._parameterDescriptors.delete(name);
8✔
1330
  }
1331

1332
  /**
1333
   * Determine if a parameter has been declared.
1334
   * @param {string} name - name of parameter
1335
   * @returns {boolean} - Return true if parameter is declared; false otherwise.
1336
   */
1337
  hasParameter(name) {
1338
    return this._parameters.has(name);
14,294✔
1339
  }
1340

1341
  /**
1342
   * Get a declared parameter by name.
1343
   *
1344
   * If unable to locate a declared parameter then a
1345
   * parameter with type == PARAMETER_NOT_SET is returned.
1346
   *
1347
   * @param {string} name - The name of the parameter.
1348
   * @return {Parameter} - The parameter.
1349
   */
1350
  getParameter(name) {
1351
    return this.getParameters([name])[0];
4,706✔
1352
  }
1353

1354
  /**
1355
   * Get a list of parameters.
1356
   *
1357
   * Find and return the declared parameters.
1358
   * If no names are provided return all declared parameters.
1359
   *
1360
   * If unable to locate a declared parameter then a
1361
   * parameter with type == PARAMETER_NOT_SET is returned in
1362
   * it's place.
1363
   *
1364
   * @param {string[]} [names] - The names of the declared parameters
1365
   *    to find or null indicating to return all declared parameters.
1366
   * @return {Parameter[]} - The parameters.
1367
   */
1368
  getParameters(names = []) {
64✔
1369
    let params = [];
9,668✔
1370

1371
    if (names.length == 0) {
9,668✔
1372
      // get all parameters
1373
      params = [...this._parameters.values()];
64✔
1374
      return params;
64✔
1375
    }
1376

1377
    for (const name of names) {
9,604✔
1378
      const param = this.hasParameter(name)
9,612!
1379
        ? this._parameters.get(name)
1380
        : new Parameter(name, ParameterType.PARAMETER_NOT_SET);
1381

1382
      params.push(param);
9,612✔
1383
    }
1384

1385
    return params;
9,604✔
1386
  }
1387

1388
  /**
1389
   * Get the types of given parameters.
1390
   *
1391
   * Return the types of given parameters.
1392
   *
1393
   * @param {string[]} [names] - The names of the declared parameters.
1394
   * @return {Uint8Array} - The types.
1395
   */
1396
  getParameterTypes(names = []) {
×
1397
    let types = [];
8✔
1398

1399
    for (const name of names) {
8✔
1400
      const descriptor = this._parameterDescriptors.get(name);
24✔
1401
      if (descriptor) {
24!
1402
        types.push(descriptor.type);
24✔
1403
      }
1404
    }
1405
    return types;
8✔
1406
  }
1407

1408
  /**
1409
   * Get the names of all declared parameters.
1410
   *
1411
   * @return {Array<string>} - The declared parameter names or empty array if
1412
   *    no parameters have been declared.
1413
   */
1414
  getParameterNames() {
1415
    return this.getParameters().map((param) => param.name);
120✔
1416
  }
1417

1418
  /**
1419
   * Determine if a parameter descriptor exists.
1420
   *
1421
   * @param {string} name - The name of a descriptor to for.
1422
   * @return {boolean} - True if a descriptor has been declared; otherwise false.
1423
   */
1424
  hasParameterDescriptor(name) {
1425
    return !!this.getParameterDescriptor(name);
4,938✔
1426
  }
1427

1428
  /**
1429
   * Get a declared parameter descriptor by name.
1430
   *
1431
   * If unable to locate a declared parameter descriptor then a
1432
   * descriptor with type == PARAMETER_NOT_SET is returned.
1433
   *
1434
   * @param {string} name - The name of the parameter descriptor to find.
1435
   * @return {ParameterDescriptor} - The parameter descriptor.
1436
   */
1437
  getParameterDescriptor(name) {
1438
    return this.getParameterDescriptors([name])[0];
9,884✔
1439
  }
1440

1441
  /**
1442
   * Find a list of declared ParameterDescriptors.
1443
   *
1444
   * If no names are provided return all declared descriptors.
1445
   *
1446
   * If unable to locate a declared descriptor then a
1447
   * descriptor with type == PARAMETER_NOT_SET is returned in
1448
   * it's place.
1449
   *
1450
   * @param {string[]} [names] - The names of the declared parameter
1451
   *    descriptors to find or null indicating to return all declared descriptors.
1452
   * @return {ParameterDescriptor[]} - The parameter descriptors.
1453
   */
1454
  getParameterDescriptors(names = []) {
×
1455
    let descriptors = [];
9,892✔
1456

1457
    if (names.length == 0) {
9,892!
1458
      // get all parameters
UNCOV
1459
      descriptors = [...this._parameterDescriptors.values()];
×
1460
      return descriptors;
×
1461
    }
1462

1463
    for (const name of names) {
9,892✔
1464
      let descriptor = this._parameterDescriptors.get(name);
9,900✔
1465
      if (!descriptor) {
9,900!
UNCOV
1466
        descriptor = new ParameterDescriptor(
×
1467
          name,
1468
          ParameterType.PARAMETER_NOT_SET
1469
        );
1470
      }
1471
      descriptors.push(descriptor);
9,900✔
1472
    }
1473

1474
    return descriptors;
9,892✔
1475
  }
1476

1477
  /**
1478
   * Replace a declared parameter.
1479
   *
1480
   * The parameter being replaced must be a declared parameter who's descriptor
1481
   * is not readOnly; otherwise an Error is thrown.
1482
   *
1483
   * @param {Parameter} parameter - The new parameter.
1484
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - The result of the operation.
1485
   */
1486
  setParameter(parameter) {
1487
    const results = this.setParameters([parameter]);
32✔
1488
    return results[0];
32✔
1489
  }
1490

1491
  /**
1492
   * Replace a list of declared parameters.
1493
   *
1494
   * Declared parameters are replaced in the order they are provided and
1495
   * a ParameterEvent is published for each individual parameter change.
1496
   *
1497
   * Prior to setting the parameters each SetParameterEventCallback registered
1498
   * using setOnParameterEventCallback() is called in succession with the parameters
1499
   * list. Any SetParameterEventCallback that retuns does not return a successful
1500
   * result will cause the entire operation to terminate with no changes to the
1501
   * parameters. When all SetParameterEventCallbacks return successful then the
1502
   * list of parameters is updated.
1503
   *
1504
   * If an error occurs, the process is stopped and returned. Parameters
1505
   * set before an error remain unchanged.
1506
   *
1507
   * @param {Parameter[]} parameters - The parameters to set.
1508
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult[]} - A list of SetParameterResult, one for each parameter that was set.
1509
   */
1510
  setParameters(parameters = []) {
×
1511
    return parameters.map((parameter) =>
40✔
1512
      this.setParametersAtomically([parameter])
40✔
1513
    );
1514
  }
1515

1516
  /**
1517
   * Repalce a list of declared parameters atomically.
1518
   *
1519
   * Declared parameters are replaced in the order they are provided.
1520
   * A single ParameterEvent is published collectively for all changed
1521
   * parameters.
1522
   *
1523
   * Prior to setting the parameters each SetParameterEventCallback registered
1524
   * using setOnParameterEventCallback() is called in succession with the parameters
1525
   * list. Any SetParameterEventCallback that retuns does not return a successful
1526
   * result will cause the entire operation to terminate with no changes to the
1527
   * parameters. When all SetParameterEventCallbacks return successful then the
1528
   * list of parameters is updated.d
1529
   *
1530
   * If an error occurs, the process stops immediately. All parameters updated to
1531
   * the point of the error are reverted to their previous state.
1532
   *
1533
   * @param {Parameter[]} parameters - The parameters to set.
1534
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - describes the result of setting 1 or more parameters.
1535
   */
1536
  setParametersAtomically(parameters = []) {
×
1537
    return this._setParametersAtomically(parameters);
48✔
1538
  }
1539

1540
  /**
1541
   * Internal method for updating parameters atomically.
1542
   *
1543
   * Prior to setting the parameters each SetParameterEventCallback registered
1544
   * using setOnParameterEventCallback() is called in succession with the parameters
1545
   * list. Any SetParameterEventCallback that retuns does not return a successful
1546
   * result will cause the entire operation to terminate with no changes to the
1547
   * parameters. When all SetParameterEventCallbacks return successful then the
1548
   * list of parameters is updated.
1549
   *
1550
   * @param {Paramerter[]} parameters - The parameters to update.
1551
   * @param {boolean} declareParameterMode - When true parameters are being declared;
1552
   *    otherwise they are being changed.
1553
   * @return {SetParameterResult} - A single collective result.
1554
   */
1555
  _setParametersAtomically(parameters = [], declareParameterMode = false) {
48!
1556
    let result = this._validateParameters(parameters, declareParameterMode);
4,938✔
1557
    if (!result.successful) {
4,938!
UNCOV
1558
      return result;
×
1559
    }
1560

1561
    // give all SetParametersCallbacks a chance to veto this change
1562
    for (const callback of this._setParametersCallbacks) {
4,938✔
1563
      result = callback(parameters);
376✔
1564
      if (!result.successful) {
376✔
1565
        // a callback has vetoed a parameter change
1566
        return result;
8✔
1567
      }
1568
    }
1569

1570
    // collectively track updates to parameters for use
1571
    // when publishing a ParameterEvent
1572
    const newParameters = [];
4,930✔
1573
    const changedParameters = [];
4,930✔
1574
    const deletedParameters = [];
4,930✔
1575

1576
    for (const parameter of parameters) {
4,930✔
1577
      if (parameter.type == ParameterType.PARAMETER_NOT_SET) {
4,930✔
1578
        this.undeclareParameter(parameter.name);
8✔
1579
        deletedParameters.push(parameter);
8✔
1580
      } else {
1581
        this._parameters.set(parameter.name, parameter);
4,922✔
1582
        if (declareParameterMode) {
4,922✔
1583
          newParameters.push(parameter);
4,890✔
1584
        } else {
1585
          changedParameters.push(parameter);
32✔
1586
        }
1587
      }
1588
    }
1589

1590
    // create ParameterEvent
1591
    const parameterEvent = new (loader.loadInterface(
4,930✔
1592
      PARAMETER_EVENT_MSG_TYPE
1593
    ))();
1594

1595
    const { seconds, nanoseconds } = this._clock.now().secondsAndNanoseconds;
4,930✔
1596
    parameterEvent.stamp = {
4,930✔
1597
      sec: Number(seconds),
1598
      nanosec: Number(nanoseconds),
1599
    };
1600

1601
    parameterEvent.node =
4,930✔
1602
      this.namespace() === '/'
4,930✔
1603
        ? this.namespace() + this.name()
1604
        : this.namespace() + '/' + this.name();
1605

1606
    if (newParameters.length > 0) {
4,930✔
1607
      parameterEvent['new_parameters'] = newParameters.map((parameter) =>
4,890✔
1608
        parameter.toParameterMessage()
4,890✔
1609
      );
1610
    }
1611
    if (changedParameters.length > 0) {
4,930✔
1612
      parameterEvent['changed_parameters'] = changedParameters.map(
32✔
1613
        (parameter) => parameter.toParameterMessage()
32✔
1614
      );
1615
    }
1616
    if (deletedParameters.length > 0) {
4,930✔
1617
      parameterEvent['deleted_parameters'] = deletedParameters.map(
8✔
1618
        (parameter) => parameter.toParameterMessage()
8✔
1619
      );
1620
    }
1621

1622
    // Publish ParameterEvent.
1623
    this._parameterEventPublisher.publish(parameterEvent);
4,930✔
1624

1625
    return {
4,930✔
1626
      successful: true,
1627
      reason: '',
1628
    };
1629
  }
1630

1631
  /**
1632
   * This callback is called when declaring a parameter or setting a parameter.
1633
   * The callback is provided a list of parameters and returns a SetParameterResult
1634
   * to indicate approval or veto of the operation.
1635
   *
1636
   * @callback SetParametersCallback
1637
   * @param {Parameter[]} parameters - The message published
1638
   * @returns {rcl_interfaces.msg.SetParameterResult} -
1639
   *
1640
   * @see [Node.addOnSetParametersCallback]{@link Node#addOnSetParametersCallback}
1641
   * @see [Node.removeOnSetParametersCallback]{@link Node#removeOnSetParametersCallback}
1642
   */
1643

1644
  /**
1645
   * Add a callback to the front of the list of callbacks invoked for parameter declaration
1646
   * and setting. No checks are made for duplicate callbacks.
1647
   *
1648
   * @param {SetParametersCallback} callback - The callback to add.
1649
   * @returns {undefined}
1650
   */
1651
  addOnSetParametersCallback(callback) {
1652
    this._setParametersCallbacks.unshift(callback);
4,674✔
1653
  }
1654

1655
  /**
1656
   * Remove a callback from the list of SetParametersCallbacks.
1657
   * If the callback is not found the process is a nop.
1658
   *
1659
   * @param {SetParametersCallback} callback - The callback to be removed
1660
   * @returns {undefined}
1661
   */
1662
  removeOnSetParametersCallback(callback) {
1663
    const idx = this._setParametersCallbacks.indexOf(callback);
16✔
1664
    if (idx > -1) {
16!
1665
      this._setParametersCallbacks.splice(idx, 1);
16✔
1666
    }
1667
  }
1668

1669
  /**
1670
   * Get the fully qualified name of the node.
1671
   *
1672
   * @returns {string} - String containing the fully qualified name of the node.
1673
   */
1674
  getFullyQualifiedName() {
1675
    return rclnodejs.getFullyQualifiedName(this.handle);
8✔
1676
  }
1677

1678
  /**
1679
   * Get the RMW implementation identifier
1680
   * @returns {string} - The RMW implementation identifier.
1681
   */
1682
  getRMWImplementationIdentifier() {
1683
    return rclnodejs.getRMWImplementationIdentifier();
8✔
1684
  }
1685

1686
  // returns on 1st error or result {successful, reason}
1687
  _validateParameters(parameters = [], declareParameterMode = false) {
×
1688
    for (const parameter of parameters) {
4,938✔
1689
      // detect invalid parameter
1690
      try {
4,938✔
1691
        parameter.validate();
4,938✔
1692
      } catch {
UNCOV
1693
        return {
×
1694
          successful: false,
1695
          reason: `Invalid ${parameter.name}`,
1696
        };
1697
      }
1698

1699
      // detect undeclared parameter
1700
      if (!this.hasParameterDescriptor(parameter.name)) {
4,938!
UNCOV
1701
        return {
×
1702
          successful: false,
1703
          reason: `Parameter ${parameter.name} has not been declared`,
1704
        };
1705
      }
1706

1707
      // detect readonly parameter that can not be updated
1708
      const descriptor = this.getParameterDescriptor(parameter.name);
4,938✔
1709
      if (!declareParameterMode && descriptor.readOnly) {
4,938!
UNCOV
1710
        return {
×
1711
          successful: false,
1712
          reason: `Parameter ${parameter.name} is readonly`,
1713
        };
1714
      }
1715

1716
      // validate parameter against descriptor if not an undeclare action
1717
      if (parameter.type != ParameterType.PARAMETER_NOT_SET) {
4,938✔
1718
        try {
4,930✔
1719
          descriptor.validateParameter(parameter);
4,930✔
1720
        } catch {
UNCOV
1721
          return {
×
1722
            successful: false,
1723
            reason: `Parameter ${parameter.name} does not  readonly`,
1724
          };
1725
        }
1726
      }
1727
    }
1728

1729
    return {
4,938✔
1730
      successful: true,
1731
      reason: null,
1732
    };
1733
  }
1734

1735
  // Get a Map(nodeName->Parameter[]) of CLI parameter args that
1736
  // apply to 'this' node, .e.g., -p mynode:foo:=bar -p hello:=world
1737
  _getNativeParameterOverrides() {
1738
    const overrides = new Map();
4,610✔
1739

1740
    // Get native parameters from rcl context->global_arguments.
1741
    // rclnodejs returns an array of objects, 1 for each node e.g., -p my_node:foo:=bar,
1742
    // and a node named '/**' for global parameter rules,
1743
    // i.e., does not include a node identifier, e.g., -p color:=red
1744
    // {
1745
    //   name: string // node name
1746
    //   parameters[] = {
1747
    //     name: string
1748
    //     type: uint
1749
    //     value: object
1750
    // }
1751
    const cliParamOverrideData = rclnodejs.getParameterOverrides(
4,610✔
1752
      this.context.handle
1753
    );
1754

1755
    // convert native CLI parameterOverrides to Map<nodeName,Array<ParameterOverride>>
1756
    const cliParamOverrides = new Map();
4,610✔
1757
    if (cliParamOverrideData) {
4,610✔
1758
      for (let nodeParamData of cliParamOverrideData) {
64✔
1759
        const nodeName = nodeParamData.name;
96✔
1760
        const nodeParamOverrides = [];
96✔
1761
        for (let paramData of nodeParamData.parameters) {
96✔
1762
          const paramOverride = new Parameter(
136✔
1763
            paramData.name,
1764
            paramData.type,
1765
            paramData.value
1766
          );
1767
          nodeParamOverrides.push(paramOverride);
136✔
1768
        }
1769
        cliParamOverrides.set(nodeName, nodeParamOverrides);
96✔
1770
      }
1771
    }
1772

1773
    // collect global CLI global parameters, name == /**
1774
    let paramOverrides = cliParamOverrides.get('/**'); // array of ParameterOverrides
4,610✔
1775
    if (paramOverrides) {
4,610✔
1776
      for (const parameter of paramOverrides) {
40✔
1777
        overrides.set(parameter.name, parameter);
48✔
1778
      }
1779
    }
1780

1781
    // merge CLI node parameterOverrides with global parameterOverrides, replace existing
1782
    paramOverrides = cliParamOverrides.get(this.name()); // array of ParameterOverrides
4,610✔
1783
    if (paramOverrides) {
4,610✔
1784
      for (const parameter of paramOverrides) {
40✔
1785
        overrides.set(parameter.name, parameter);
56✔
1786
      }
1787
    }
1788

1789
    return overrides;
4,610✔
1790
  }
1791

1792
  /**
1793
   * Invokes the callback with a raw message of the given type. After the callback completes
1794
   * the message will be destroyed.
1795
   * @param {function} Type - Message type to create.
1796
   * @param {function} callback - Callback to invoke. First parameter will be the raw message,
1797
   * and the second is a function to retrieve the deserialized message.
1798
   * @returns {undefined}
1799
   */
1800
  _runWithMessageType(Type, callback) {
1801
    let message = new Type();
5,561✔
1802

1803
    callback(message.toRawROS(), () => {
5,561✔
1804
      let result = new Type();
4,378✔
1805
      result.deserialize(message.refObject);
4,378✔
1806

1807
      return result;
4,378✔
1808
    });
1809

1810
    Type.destoryRawROS(message);
5,561✔
1811
  }
1812

1813
  _addActionClient(actionClient) {
1814
    this._actionClients.push(actionClient);
324✔
1815
    this.syncHandles();
324✔
1816
  }
1817

1818
  _addActionServer(actionServer) {
1819
    this._actionServers.push(actionServer);
324✔
1820
    this.syncHandles();
324✔
1821
  }
1822
}
1823

1824
/**
1825
 * Create an Options instance initialized with default values.
1826
 * @returns {Options} - The new initialized instance.
1827
 * @static
1828
 * @example
1829
 * {
1830
 *   enableTypedArray: true,
1831
 *   isRaw: false,
1832
 *   qos: QoS.profileDefault,
1833
 *   contentFilter: undefined,
1834
 * }
1835
 */
1836
Node.getDefaultOptions = function () {
208✔
1837
  return {
42,699✔
1838
    enableTypedArray: true,
1839
    isRaw: false,
1840
    qos: QoS.profileDefault,
1841
    contentFilter: undefined,
1842
  };
1843
};
1844

1845
module.exports = Node;
208✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc