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

RobotWebTools / rclnodejs / 15456576549

05 Jun 2025 01:51AM UTC coverage: 84.252% (-0.6%) from 84.823%
15456576549

Pull #1159

github

web-flow
Merge 4a486f531 into 0dbe3da92
Pull Request #1159: Implement event handler

763 of 997 branches covered (76.53%)

Branch coverage included in aggregate %.

91 of 124 new or added lines in 4 files covered. (73.39%)

42 existing lines in 1 file now uncovered.

1880 of 2140 relevant lines covered (87.85%)

2791.45 hits per line

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

88.49
/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
    if (this._context.isShutdown()) {
35,978!
NEW
UNCOV
167
      console.log('Node is shutdown, skip executing handles.');
×
NEW
UNCOV
168
      return;
×
169
    }
170
    let timersReady = this._timers.filter((timer) =>
35,978✔
171
      handles.includes(timer.handle)
30,665✔
172
    );
173
    let guardsReady = this._guards.filter((guard) =>
35,978✔
174
      handles.includes(guard.handle)
24✔
175
    );
176
    let subscriptionsReady = this._subscriptions.filter((subscription) =>
35,978✔
177
      handles.includes(subscription.handle)
3,765✔
178
    );
179
    let clientsReady = this._clients.filter((client) =>
35,978✔
180
      handles.includes(client.handle)
732✔
181
    );
182
    let servicesReady = this._services.filter((service) =>
35,978✔
183
      handles.includes(service.handle)
101,202✔
184
    );
185
    let actionClientsReady = this._actionClients.filter((actionClient) =>
35,978✔
186
      handles.includes(actionClient.handle)
1,250✔
187
    );
188
    let actionServersReady = this._actionServers.filter((actionServer) =>
35,978✔
189
      handles.includes(actionServer.handle)
1,250✔
190
    );
191
    let eventsReady = this._events.filter((event) =>
35,978✔
192
      handles.includes(event.handle)
24✔
193
    );
194

195
    timersReady.forEach((timer) => {
35,978✔
196
      if (timer.isReady()) {
30,585✔
197
        rclnodejs.callTimer(timer.handle);
30,284✔
198
        timer.callback();
30,284✔
199
      }
200
    });
201

202
    eventsReady.forEach((event) => {
35,978✔
203
      if (!this._spinning) {
24!
NEW
UNCOV
204
        return;
×
205
      }
206
      event.takeData();
24✔
207
    });
208

209
    for (const subscription of subscriptionsReady) {
35,978✔
210
      if (subscription.isDestroyed()) continue;
3,479✔
211
      if (subscription.isRaw) {
3,399✔
212
        let rawMessage = rclnodejs.rclTakeRaw(subscription.handle);
8✔
213
        if (rawMessage) {
8!
214
          subscription.processResponse(rawMessage);
8✔
215
        }
216
        continue;
8✔
217
      }
218

219
      this._runWithMessageType(
3,391✔
220
        subscription.typeClass,
221
        (message, deserialize) => {
222
          let success = rclnodejs.rclTake(subscription.handle, message);
3,391✔
223
          if (success) {
3,391✔
224
            subscription.processResponse(deserialize());
2,824✔
225
          }
226
        }
227
      );
228
    }
229

230
    for (const guard of guardsReady) {
35,978✔
231
      if (guard.isDestroyed()) continue;
24!
232

233
      guard.callback();
24✔
234
    }
235

236
    for (const client of clientsReady) {
35,978✔
237
      if (client.isDestroyed()) continue;
378!
238
      this._runWithMessageType(
378✔
239
        client.typeClass.Response,
240
        (message, deserialize) => {
241
          let sequenceNumber = rclnodejs.rclTakeResponse(
378✔
242
            client.handle,
243
            message
244
          );
245
          if (sequenceNumber !== undefined) {
378✔
246
            client.processResponse(sequenceNumber, deserialize());
232✔
247
          }
248
        }
249
      );
250
    }
251

252
    for (const service of servicesReady) {
35,978✔
253
      if (service.isDestroyed()) continue;
463!
254
      this._runWithMessageType(
463✔
255
        service.typeClass.Request,
256
        (message, deserialize) => {
257
          let header = rclnodejs.rclTakeRequest(
463✔
258
            service.handle,
259
            this.handle,
260
            message
261
          );
262
          if (header) {
463✔
263
            service.processRequest(header, deserialize());
240✔
264
          }
265
        }
266
      );
267
    }
268

269
    for (const actionClient of actionClientsReady) {
35,978✔
270
      if (actionClient.isDestroyed()) continue;
592!
271

272
      const properties = actionClient.handle.properties;
592✔
273

274
      if (properties.isGoalResponseReady) {
592✔
275
        this._runWithMessageType(
286✔
276
          actionClient.typeClass.impl.SendGoalService.Response,
277
          (message, deserialize) => {
278
            let sequence = rclnodejs.actionTakeGoalResponse(
286✔
279
              actionClient.handle,
280
              message
281
            );
282
            if (sequence != undefined) {
286✔
283
              actionClient.processGoalResponse(sequence, deserialize());
240✔
284
            }
285
          }
286
        );
287
      }
288

289
      if (properties.isCancelResponseReady) {
592✔
290
        this._runWithMessageType(
44✔
291
          actionClient.typeClass.impl.CancelGoal.Response,
292
          (message, deserialize) => {
293
            let sequence = rclnodejs.actionTakeCancelResponse(
44✔
294
              actionClient.handle,
295
              message
296
            );
297
            if (sequence != undefined) {
44✔
298
              actionClient.processCancelResponse(sequence, deserialize());
32✔
299
            }
300
          }
301
        );
302
      }
303

304
      if (properties.isResultResponseReady) {
592✔
305
        this._runWithMessageType(
125✔
306
          actionClient.typeClass.impl.GetResultService.Response,
307
          (message, deserialize) => {
308
            let sequence = rclnodejs.actionTakeResultResponse(
125✔
309
              actionClient.handle,
310
              message
311
            );
312
            if (sequence != undefined) {
125✔
313
              actionClient.processResultResponse(sequence, deserialize());
120✔
314
            }
315
          }
316
        );
317
      }
318

319
      if (properties.isFeedbackReady) {
592✔
320
        this._runWithMessageType(
58✔
321
          actionClient.typeClass.impl.FeedbackMessage,
322
          (message, deserialize) => {
323
            let success = rclnodejs.actionTakeFeedback(
58✔
324
              actionClient.handle,
325
              message
326
            );
327
            if (success) {
58✔
328
              actionClient.processFeedbackMessage(deserialize());
40✔
329
            }
330
          }
331
        );
332
      }
333

334
      if (properties.isStatusReady) {
592✔
335
        this._runWithMessageType(
318✔
336
          actionClient.typeClass.impl.GoalStatusArray,
337
          (message, deserialize) => {
338
            let success = rclnodejs.actionTakeStatus(
318✔
339
              actionClient.handle,
340
              message
341
            );
342
            if (success) {
318✔
343
              actionClient.processStatusMessage(deserialize());
271✔
344
            }
345
          }
346
        );
347
      }
348
    }
349

350
    for (const actionServer of actionServersReady) {
35,978✔
351
      if (actionServer.isDestroyed()) continue;
735!
352

353
      const properties = actionServer.handle.properties;
735✔
354

355
      if (properties.isGoalRequestReady) {
735✔
356
        this._runWithMessageType(
258✔
357
          actionServer.typeClass.impl.SendGoalService.Request,
358
          (message, deserialize) => {
359
            const result = rclnodejs.actionTakeGoalRequest(
258✔
360
              actionServer.handle,
361
              message
362
            );
363
            if (result) {
258✔
364
              actionServer.processGoalRequest(result, deserialize());
240✔
365
            }
366
          }
367
        );
368
      }
369

370
      if (properties.isCancelRequestReady) {
735✔
371
        this._runWithMessageType(
57✔
372
          actionServer.typeClass.impl.CancelGoal.Request,
373
          (message, deserialize) => {
374
            const result = rclnodejs.actionTakeCancelRequest(
57✔
375
              actionServer.handle,
376
              message
377
            );
378
            if (result) {
57✔
379
              actionServer.processCancelRequest(result, deserialize());
32✔
380
            }
381
          }
382
        );
383
      }
384

385
      if (properties.isResultRequestReady) {
735✔
386
        this._runWithMessageType(
180✔
387
          actionServer.typeClass.impl.GetResultService.Request,
388
          (message, deserialize) => {
389
            const result = rclnodejs.actionTakeResultRequest(
180✔
390
              actionServer.handle,
391
              message
392
            );
393
            if (result) {
180✔
394
              actionServer.processResultRequest(result, deserialize());
120✔
395
            }
396
          }
397
        );
398
      }
399

400
      if (properties.isGoalExpired) {
735✔
401
        let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
32✔
402
        let message = new GoalInfoArray(actionServer._goalHandles.size);
32✔
403
        let count = rclnodejs.actionExpireGoals(
32✔
404
          actionServer.handle,
405
          actionServer._goalHandles.size,
406
          message._refArray.buffer
407
        );
408
        if (count > 0) {
32✔
409
          actionServer.processGoalExpired(message, count);
24✔
410
        }
411
        GoalInfoArray.freeArray(message);
32✔
412
      }
413
    }
414

415
    // At this point it is safe to clear the cache of any
416
    // destroyed entity references
417
    Entity._gcHandles();
35,978✔
418
  }
419

420
  /**
421
   * Determine if this node is spinning.
422
   * @returns {boolean} - true when spinning; otherwise returns false.
423
   */
424
  get spinning() {
425
    return this._spinning;
32,280✔
426
  }
427

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

444
  /**
445
   * Use spin().
446
   * @deprecated, since 0.18.0
447
   */
448
  startSpinning(timeout) {
UNCOV
449
    this.spin(timeout);
×
450
  }
451

452
  /**
453
   * Terminate spinning - no further events will be received.
454
   * @returns {undefined}
455
   */
456
  stop() {
457
    super.stop();
3,406✔
458
    this._spinning = false;
3,406✔
459
  }
460

461
  /**
462
   * Terminate spinning - no further events will be received.
463
   * @returns {undefined}
464
   * @deprecated since 0.18.0, Use stop().
465
   */
466
  stopSpinning() {
UNCOV
467
    super.stop();
×
UNCOV
468
    this._spinning = false;
×
469
  }
470

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

486
  _removeEntityFromArray(entity, array) {
487
    let index = array.indexOf(entity);
928✔
488
    if (index > -1) {
928✔
489
      array.splice(index, 1);
912✔
490
    }
491
  }
492

493
  _destroyEntity(entity, array, syncHandles = true) {
938✔
494
    if (entity['isDestroyed'] && entity.isDestroyed()) return;
992✔
495

496
    this._removeEntityFromArray(entity, array);
928✔
497
    if (syncHandles) {
928✔
498
      this.syncHandles();
890✔
499
    }
500

501
    if (entity['_destroy']) {
928✔
502
      entity._destroy();
880✔
503
    } else {
504
      // guards and timers
505
      entity.handle.release();
48✔
506
    }
507
  }
508

509
  _validateOptions(options) {
510
    if (
42,890✔
511
      options !== undefined &&
43,274✔
512
      (options === null || typeof options !== 'object')
513
    ) {
514
      throw new TypeError('Invalid argument of options');
160✔
515
    }
516

517
    if (options === undefined) {
42,730✔
518
      return Node.getDefaultOptions();
42,618✔
519
    }
520

521
    if (options.enableTypedArray === undefined) {
112✔
522
      options = Object.assign(options, { enableTypedArray: true });
16✔
523
    }
524

525
    if (options.qos === undefined) {
112✔
526
      options = Object.assign(options, { qos: QoS.profileDefault });
32✔
527
    }
528

529
    if (options.isRaw === undefined) {
112✔
530
      options = Object.assign(options, { isRaw: false });
32✔
531
    }
532

533
    return options;
112✔
534
  }
535

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

550
    if (typeof period !== 'bigint' || typeof callback !== 'function') {
462✔
551
      throw new TypeError('Invalid argument');
16✔
552
    }
553

554
    const timerClock = clock || this._clock;
446✔
555
    let timerHandle = rclnodejs.createTimer(
446✔
556
      timerClock.handle,
557
      this.context.handle,
558
      period
559
    );
560
    let timer = new Timer(timerHandle, period, callback);
446✔
561
    debug('Finish creating timer, period = %d.', period);
446✔
562
    this._timers.push(timer);
446✔
563
    this.syncHandles();
446✔
564

565
    return timer;
446✔
566
  }
567

568
  /**
569
   * Create a Rate.
570
   *
571
   * @param {number} hz - The frequency of the rate timer; default is 1 hz.
572
   * @returns {Promise<Rate>} - Promise resolving to new instance of Rate.
573
   */
574
  async createRate(hz = 1) {
32✔
575
    if (typeof hz !== 'number') {
72!
UNCOV
576
      throw new TypeError('Invalid argument');
×
577
    }
578

579
    const MAX_RATE_HZ_IN_MILLISECOND = 1000.0;
72✔
580
    if (hz <= 0.0 || hz > MAX_RATE_HZ_IN_MILLISECOND) {
72✔
581
      throw new RangeError(
16✔
582
        `Hz must be between 0.0 and ${MAX_RATE_HZ_IN_MILLISECOND}`
583
      );
584
    }
585

586
    // lazy initialize rateTimerServer
587
    if (!this._rateTimerServer) {
56✔
588
      this._rateTimerServer = new Rates.RateTimerServer(this);
40✔
589
      await this._rateTimerServer.init();
40✔
590
    }
591

592
    const period = Math.round(1000 / hz);
56✔
593
    const timer = this._rateTimerServer.createTimer(BigInt(period) * 1000000n);
56✔
594
    const rate = new Rates.Rate(hz, timer);
56✔
595

596
    return rate;
56✔
597
  }
598

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

620
  _createPublisher(typeClass, topic, options, publisherClass, eventCallbacks) {
621
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
7,506✔
622
      typeClass = loader.loadInterface(typeClass);
7,314✔
623
    }
624
    options = this._validateOptions(options);
7,450✔
625

626
    if (
7,450✔
627
      typeof typeClass !== 'function' ||
22,138✔
628
      typeof topic !== 'string' ||
629
      (eventCallbacks && !(eventCallbacks instanceof PublisherEventCallbacks))
630
    ) {
631
      throw new TypeError('Invalid argument');
160✔
632
    }
633

634
    let publisher = publisherClass.createPublisher(
7,290✔
635
      this,
636
      typeClass,
637
      topic,
638
      options,
639
      eventCallbacks
640
    );
641
    debug('Finish creating publisher, topic = %s.', topic);
7,210✔
642
    this._publishers.push(publisher);
7,210✔
643
    return publisher;
7,210✔
644
  }
645

646
  /**
647
   * This callback is called when a message is published
648
   * @callback SubscriptionCallback
649
   * @param {Object} message - The message published
650
   * @see [Node.createSubscription]{@link Node#createSubscription}
651
   * @see [Node.createPublisher]{@link Node#createPublisher}
652
   * @see {@link Publisher}
653
   * @see {@link Subscription}
654
   */
655

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

685
    if (typeof options === 'function') {
2,960✔
686
      callback = options;
2,682✔
687
      options = undefined;
2,682✔
688
    }
689
    options = this._validateOptions(options);
2,960✔
690

691
    if (
2,880✔
692
      typeof typeClass !== 'function' ||
11,352✔
693
      typeof topic !== 'string' ||
694
      typeof callback !== 'function' ||
695
      (eventCallbacks &&
696
        !(eventCallbacks instanceof SubscriptionEventCallbacks))
697
    ) {
698
      throw new TypeError('Invalid argument');
80✔
699
    }
700

701
    let subscription = Subscription.createSubscription(
2,800✔
702
      this,
703
      typeClass,
704
      topic,
705
      options,
706
      callback,
707
      eventCallbacks
708
    );
709
    debug('Finish creating subscription, topic = %s.', topic);
2,712✔
710
    this._subscriptions.push(subscription);
2,712✔
711
    this.syncHandles();
2,712✔
712

713
    return subscription;
2,712✔
714
  }
715

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

733
    if (typeof typeClass !== 'function' || typeof serviceName !== 'string') {
596✔
734
      throw new TypeError('Invalid argument');
160✔
735
    }
736

737
    let client = Client.createClient(
436✔
738
      this.handle,
739
      serviceName,
740
      typeClass,
741
      options
742
    );
743
    debug('Finish creating client, service = %s.', serviceName);
356✔
744
    this._clients.push(client);
356✔
745
    this.syncHandles();
356✔
746

747
    return client;
356✔
748
  }
749

750
  /**
751
   * This callback is called when a request is sent to service
752
   * @callback RequestCallback
753
   * @param {Object} request - The request sent to the service
754
   * @param {Response} response - The response to client.
755
        Use [response.send()]{@link Response#send} to send response object to client
756
   * @return {undefined}
757
   * @see [Node.createService]{@link Node#createService}
758
   * @see [Client.sendRequest]{@link Client#sendRequest}
759
   * @see {@link Client}
760
   * @see {@link Service}
761
   * @see {@link Response#send}
762
   */
763

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

782
    if (typeof options === 'function') {
27,860✔
783
      callback = options;
27,684✔
784
      options = undefined;
27,684✔
785
    }
786
    options = this._validateOptions(options);
27,860✔
787

788
    if (
27,780✔
789
      typeof typeClass !== 'function' ||
83,228✔
790
      typeof serviceName !== 'string' ||
791
      typeof callback !== 'function'
792
    ) {
793
      throw new TypeError('Invalid argument');
80✔
794
    }
795

796
    let service = Service.createService(
27,700✔
797
      this.handle,
798
      serviceName,
799
      typeClass,
800
      options,
801
      callback
802
    );
803
    debug('Finish creating service, service = %s.', serviceName);
27,620✔
804
    this._services.push(service);
27,620✔
805
    this.syncHandles();
27,620✔
806

807
    return service;
27,620✔
808
  }
809

810
  /**
811
   * Create a guard condition.
812
   * @param {Function} callback - The callback to be called when the guard condition is triggered.
813
   * @return {GuardCondition} - An instance of GuardCondition.
814
   */
815
  createGuardCondition(callback) {
816
    if (typeof callback !== 'function') {
24!
UNCOV
817
      throw new TypeError('Invalid argument');
×
818
    }
819

820
    let guard = GuardCondition.createGuardCondition(callback, this.context);
24✔
821
    debug('Finish creating guard condition');
24✔
822
    this._guards.push(guard);
24✔
823
    this.syncHandles();
24✔
824

825
    return guard;
24✔
826
  }
827

828
  /**
829
   * Destroy all resource allocated by this node, including
830
   * <code>Timer</code>s/<code>Publisher</code>s/<code>Subscription</code>s
831
   * /<code>Client</code>s/<code>Service</code>s
832
   * @return {undefined}
833
   */
834
  destroy() {
835
    if (this.spinning) {
4,786✔
836
      this.stop();
3,386✔
837
    }
838

839
    // Action servers/clients require manual destruction due to circular reference with goal handles.
840
    this._actionClients.forEach((actionClient) => actionClient.destroy());
4,786✔
841
    this._actionServers.forEach((actionServer) => actionServer.destroy());
4,786✔
842

843
    this.context.onNodeDestroyed(this);
4,786✔
844

845
    this.handle.release();
4,786✔
846
    this._clock = null;
4,786✔
847
    this._timers = [];
4,786✔
848
    this._publishers = [];
4,786✔
849
    this._subscriptions = [];
4,786✔
850
    this._clients = [];
4,786✔
851
    this._services = [];
4,786✔
852
    this._guards = [];
4,786✔
853
    this._actionClients = [];
4,786✔
854
    this._actionServers = [];
4,786✔
855

856
    if (this._rateTimerServer) {
4,786✔
857
      this._rateTimerServer.shutdown();
40✔
858
      this._rateTimerServer = null;
40✔
859
    }
860
  }
861

862
  /**
863
   * Destroy a Publisher.
864
   * @param {Publisher} publisher - The Publisher to be destroyed.
865
   * @return {undefined}
866
   */
867
  destroyPublisher(publisher) {
868
    if (!(publisher instanceof Publisher)) {
70✔
869
      throw new TypeError('Invalid argument');
16✔
870
    }
871
    if (publisher._eventHandlers) {
54!
NEW
UNCOV
872
      publisher._eventHandlers.forEach((handler) => {
×
NEW
UNCOV
873
        this._destroyEntity(handler, this._events);
×
874
      });
NEW
UNCOV
875
      publisher._eventHandlers = [];
×
876
    }
877
    this._destroyEntity(publisher, this._publishers, false);
54✔
878
  }
879

880
  /**
881
   * Destroy a Subscription.
882
   * @param {Subscription} subscription - The Subscription to be destroyed.
883
   * @return {undefined}
884
   */
885
  destroySubscription(subscription) {
886
    if (!(subscription instanceof Subscription)) {
180✔
887
      throw new TypeError('Invalid argument');
16✔
888
    }
889
    if (subscription._eventHandlers) {
164✔
890
      subscription._eventHandlers.forEach((handler) => {
6✔
891
        this._destroyEntity(handler, this._events);
6✔
892
      });
893
      subscription._eventHandlers = [];
6✔
894
    }
895

896
    this._destroyEntity(subscription, this._subscriptions);
164✔
897
  }
898

899
  /**
900
   * Destroy a Client.
901
   * @param {Client} client - The Client to be destroyed.
902
   * @return {undefined}
903
   */
904
  destroyClient(client) {
905
    if (!(client instanceof Client)) {
64✔
906
      throw new TypeError('Invalid argument');
16✔
907
    }
908
    this._destroyEntity(client, this._clients);
48✔
909
  }
910

911
  /**
912
   * Destroy a Service.
913
   * @param {Service} service - The Service to be destroyed.
914
   * @return {undefined}
915
   */
916
  destroyService(service) {
917
    if (!(service instanceof Service)) {
64✔
918
      throw new TypeError('Invalid argument');
16✔
919
    }
920
    this._destroyEntity(service, this._services);
48✔
921
  }
922

923
  /**
924
   * Destroy a Timer.
925
   * @param {Timer} timer - The Timer to be destroyed.
926
   * @return {undefined}
927
   */
928
  destroyTimer(timer) {
929
    if (!(timer instanceof Timer)) {
64✔
930
      throw new TypeError('Invalid argument');
16✔
931
    }
932
    this._destroyEntity(timer, this._timers);
48✔
933
  }
934

935
  /**
936
   * Destroy a guard condition.
937
   * @param {GuardCondition} guard - The guard condition to be destroyed.
938
   * @return {undefined}
939
   */
940
  destroyGuardCondition(guard) {
941
    if (!(guard instanceof GuardCondition)) {
24!
UNCOV
942
      throw new TypeError('Invalid argument');
×
943
    }
944
    this._destroyEntity(guard, this._guards);
24✔
945
  }
946

947
  /**
948
   * Get the name of the node.
949
   * @return {string}
950
   */
951
  name() {
952
    return rclnodejs.getNodeName(this.handle);
17,766✔
953
  }
954

955
  /**
956
   * Get the namespace of the node.
957
   * @return {string}
958
   */
959
  namespace() {
960
    return rclnodejs.getNamespace(this.handle);
10,076✔
961
  }
962

963
  /**
964
   * Get the context in which this node was created.
965
   * @return {Context}
966
   */
967
  get context() {
968
    return this._context;
41,938✔
969
  }
970

971
  /**
972
   * Get the nodes logger.
973
   * @returns {Logger} - The logger for the node.
974
   */
975
  getLogger() {
976
    return this._logger;
1,128✔
977
  }
978

979
  /**
980
   * Get the clock used by the node.
981
   * @returns {Clock} - The nodes clock.
982
   */
983
  getClock() {
984
    return this._clock;
656✔
985
  }
986

987
  /**
988
   * Get the current time using the node's clock.
989
   * @returns {Time} - The current time.
990
   */
991
  now() {
992
    return this.getClock().now();
16✔
993
  }
994

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

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

1027
  /**
1028
   * Get service names and types for which a remote node has servers.
1029
   * @param {string} nodeName - The name of the node.
1030
   * @param {string} namespace - The name of the namespace.
1031
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1032
   */
1033
  getServiceNamesAndTypesByNode(nodeName, namespace) {
UNCOV
1034
    return rclnodejs.getServiceNamesAndTypesByNode(
×
1035
      this.handle,
1036
      nodeName,
1037
      namespace
1038
    );
1039
  }
1040

1041
  /**
1042
   * Get service names and types for which a remote node has clients.
1043
   * @param {string} nodeName - The name of the node.
1044
   * @param {string} namespace - The name of the namespace.
1045
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1046
   */
1047
  getClientNamesAndTypesByNode(nodeName, namespace) {
UNCOV
1048
    return rclnodejs.getClientNamesAndTypesByNode(
×
1049
      this.handle,
1050
      nodeName,
1051
      namespace
1052
    );
1053
  }
1054

1055
  /**
1056
   * Get the list of topics discovered by the provided node.
1057
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1058
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1059
   */
1060
  getTopicNamesAndTypes(noDemangle = false) {
×
UNCOV
1061
    return rclnodejs.getTopicNamesAndTypes(this.handle, noDemangle);
×
1062
  }
1063

1064
  /**
1065
   * Get the list of services discovered by the provided node.
1066
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1067
   */
1068
  getServiceNamesAndTypes() {
1069
    return rclnodejs.getServiceNamesAndTypes(this.handle);
16✔
1070
  }
1071

1072
  /**
1073
   * Get a list of publishers on a given topic.
1074
   * @param {string} topic - the topic name to get the publishers for.
1075
   * @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name,
1076
   *       otherwise it should be a valid ROS topic name.
1077
   * @returns {Array} - list of publishers
1078
   */
1079
  getPublishersInfoByTopic(topic, noDemangle) {
1080
    return rclnodejs.getPublishersInfoByTopic(this.handle, topic, noDemangle);
14✔
1081
  }
1082

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

1098
  /**
1099
   * Get the list of nodes discovered by the provided node.
1100
   * @return {Array<string>} - An array of the names.
1101
   */
1102
  getNodeNames() {
1103
    return this.getNodeNamesAndNamespaces().map((item) => item.name);
320✔
1104
  }
1105

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

1114
  /**
1115
   * Get the list of nodes and their namespaces with enclaves discovered by the provided node.
1116
   * @return {Array<{name: string, namespace: string, enclave: string}>} An array of the names, namespaces and enclaves.
1117
   */
1118
  getNodeNamesAndNamespacesWithEnclaves() {
1119
    return rclnodejs.getNodeNames(this.handle, /*getEnclaves=*/ true);
8✔
1120
  }
1121

1122
  /**
1123
   * Return the number of publishers on a given topic.
1124
   * @param {string} topic - The name of the topic.
1125
   * @returns {number} - Number of publishers on the given topic.
1126
   */
1127
  countPublishers(topic) {
1128
    let expandedTopic = rclnodejs.expandTopicName(
48✔
1129
      topic,
1130
      this.name(),
1131
      this.namespace()
1132
    );
1133
    rclnodejs.validateTopicName(expandedTopic);
48✔
1134

1135
    return rclnodejs.countPublishers(this.handle, expandedTopic);
48✔
1136
  }
1137

1138
  /**
1139
   * Return the number of subscribers on a given topic.
1140
   * @param {string} topic - The name of the topic.
1141
   * @returns {number} - Number of subscribers on the given topic.
1142
   */
1143
  countSubscribers(topic) {
1144
    let expandedTopic = rclnodejs.expandTopicName(
48✔
1145
      topic,
1146
      this.name(),
1147
      this.namespace()
1148
    );
1149
    rclnodejs.validateTopicName(expandedTopic);
48✔
1150

1151
    return rclnodejs.countSubscribers(this.handle, expandedTopic);
48✔
1152
  }
1153

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

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

1180
  /**
1181
   * Get the list of parameter-overrides found on the commandline and
1182
   * in the NodeOptions.parameter_overrides property.
1183
   *
1184
   * @return {Array<Parameter>} - An array of Parameters.
1185
   */
1186
  getParameterOverrides() {
1187
    return Array.from(this._parameterOverrides.values());
64✔
1188
  }
1189

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

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

1265
    const declaredDescriptors = [];
4,890✔
1266
    const declaredParameters = [];
4,890✔
1267
    const declaredParameterCollisions = [];
4,890✔
1268
    for (let i = 0; i < parameters.length; i++) {
4,890✔
1269
      let parameter =
1270
        !ignoreOverrides && this._parameterOverrides.has(parameters[i].name)
4,890✔
1271
          ? this._parameterOverrides.get(parameters[i].name)
1272
          : parameters[i];
1273

1274
      // stop processing parameters that have already been declared
1275
      if (this._parameters.has(parameter.name)) {
4,890!
UNCOV
1276
        declaredParameterCollisions.push(parameter);
×
UNCOV
1277
        continue;
×
1278
      }
1279

1280
      // create descriptor for parameter if not provided
1281
      let descriptor =
1282
        descriptors.length > 0
4,890✔
1283
          ? descriptors[i]
1284
          : ParameterDescriptor.fromParameter(parameter);
1285

1286
      descriptor.validate();
4,890✔
1287

1288
      declaredDescriptors.push(descriptor);
4,890✔
1289
      declaredParameters.push(parameter);
4,890✔
1290
    }
1291

1292
    if (declaredParameterCollisions.length > 0) {
4,890!
1293
      const errorMsg =
UNCOV
1294
        declaredParameterCollisions.length == 1
×
1295
          ? `Parameter(${declaredParameterCollisions[0]}) already declared.`
1296
          : `Multiple parameters already declared, e.g., Parameter(${declaredParameterCollisions[0]}).`;
UNCOV
1297
      throw new Error(errorMsg);
×
1298
    }
1299

1300
    // register descriptor
1301
    for (const descriptor of declaredDescriptors) {
4,890✔
1302
      this._parameterDescriptors.set(descriptor.name, descriptor);
4,890✔
1303
    }
1304

1305
    const result = this._setParametersAtomically(declaredParameters, true);
4,890✔
1306
    if (!result.successful) {
4,890!
1307
      // unregister descriptors
UNCOV
1308
      for (const descriptor of declaredDescriptors) {
×
UNCOV
1309
        this._parameterDescriptors.delete(descriptor.name);
×
1310
      }
1311

UNCOV
1312
      throw new Error(result.reason);
×
1313
    }
1314

1315
    return this.getParameters(declaredParameters.map((param) => param.name));
4,890✔
1316
  }
1317

1318
  /**
1319
   * Undeclare a parameter.
1320
   *
1321
   * Readonly parameters can not be undeclared or updated.
1322
   * @param {string} name - Name of parameter to undeclare.
1323
   * @return {undefined} -
1324
   */
1325
  undeclareParameter(name) {
1326
    if (!this.hasParameter(name)) return;
8!
1327

1328
    const descriptor = this.getParameterDescriptor(name);
8✔
1329
    if (descriptor.readOnly) {
8!
UNCOV
1330
      throw new Error(
×
1331
        `${name} parameter is read-only and can not be undeclared`
1332
      );
1333
    }
1334

1335
    this._parameters.delete(name);
8✔
1336
    this._parameterDescriptors.delete(name);
8✔
1337
  }
1338

1339
  /**
1340
   * Determine if a parameter has been declared.
1341
   * @param {string} name - name of parameter
1342
   * @returns {boolean} - Return true if parameter is declared; false otherwise.
1343
   */
1344
  hasParameter(name) {
1345
    return this._parameters.has(name);
14,294✔
1346
  }
1347

1348
  /**
1349
   * Get a declared parameter by name.
1350
   *
1351
   * If unable to locate a declared parameter then a
1352
   * parameter with type == PARAMETER_NOT_SET is returned.
1353
   *
1354
   * @param {string} name - The name of the parameter.
1355
   * @return {Parameter} - The parameter.
1356
   */
1357
  getParameter(name) {
1358
    return this.getParameters([name])[0];
4,706✔
1359
  }
1360

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

1378
    if (names.length == 0) {
9,668✔
1379
      // get all parameters
1380
      params = [...this._parameters.values()];
64✔
1381
      return params;
64✔
1382
    }
1383

1384
    for (const name of names) {
9,604✔
1385
      const param = this.hasParameter(name)
9,612!
1386
        ? this._parameters.get(name)
1387
        : new Parameter(name, ParameterType.PARAMETER_NOT_SET);
1388

1389
      params.push(param);
9,612✔
1390
    }
1391

1392
    return params;
9,604✔
1393
  }
1394

1395
  /**
1396
   * Get the types of given parameters.
1397
   *
1398
   * Return the types of given parameters.
1399
   *
1400
   * @param {string[]} [names] - The names of the declared parameters.
1401
   * @return {Uint8Array} - The types.
1402
   */
1403
  getParameterTypes(names = []) {
×
1404
    let types = [];
8✔
1405

1406
    for (const name of names) {
8✔
1407
      const descriptor = this._parameterDescriptors.get(name);
24✔
1408
      if (descriptor) {
24!
1409
        types.push(descriptor.type);
24✔
1410
      }
1411
    }
1412
    return types;
8✔
1413
  }
1414

1415
  /**
1416
   * Get the names of all declared parameters.
1417
   *
1418
   * @return {Array<string>} - The declared parameter names or empty array if
1419
   *    no parameters have been declared.
1420
   */
1421
  getParameterNames() {
1422
    return this.getParameters().map((param) => param.name);
120✔
1423
  }
1424

1425
  /**
1426
   * Determine if a parameter descriptor exists.
1427
   *
1428
   * @param {string} name - The name of a descriptor to for.
1429
   * @return {boolean} - True if a descriptor has been declared; otherwise false.
1430
   */
1431
  hasParameterDescriptor(name) {
1432
    return !!this.getParameterDescriptor(name);
4,938✔
1433
  }
1434

1435
  /**
1436
   * Get a declared parameter descriptor by name.
1437
   *
1438
   * If unable to locate a declared parameter descriptor then a
1439
   * descriptor with type == PARAMETER_NOT_SET is returned.
1440
   *
1441
   * @param {string} name - The name of the parameter descriptor to find.
1442
   * @return {ParameterDescriptor} - The parameter descriptor.
1443
   */
1444
  getParameterDescriptor(name) {
1445
    return this.getParameterDescriptors([name])[0];
9,884✔
1446
  }
1447

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

1464
    if (names.length == 0) {
9,892!
1465
      // get all parameters
UNCOV
1466
      descriptors = [...this._parameterDescriptors.values()];
×
1467
      return descriptors;
×
1468
    }
1469

1470
    for (const name of names) {
9,892✔
1471
      let descriptor = this._parameterDescriptors.get(name);
9,900✔
1472
      if (!descriptor) {
9,900!
UNCOV
1473
        descriptor = new ParameterDescriptor(
×
1474
          name,
1475
          ParameterType.PARAMETER_NOT_SET
1476
        );
1477
      }
1478
      descriptors.push(descriptor);
9,900✔
1479
    }
1480

1481
    return descriptors;
9,892✔
1482
  }
1483

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

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

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

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

1568
    // give all SetParametersCallbacks a chance to veto this change
1569
    for (const callback of this._setParametersCallbacks) {
4,938✔
1570
      result = callback(parameters);
376✔
1571
      if (!result.successful) {
376✔
1572
        // a callback has vetoed a parameter change
1573
        return result;
8✔
1574
      }
1575
    }
1576

1577
    // collectively track updates to parameters for use
1578
    // when publishing a ParameterEvent
1579
    const newParameters = [];
4,930✔
1580
    const changedParameters = [];
4,930✔
1581
    const deletedParameters = [];
4,930✔
1582

1583
    for (const parameter of parameters) {
4,930✔
1584
      if (parameter.type == ParameterType.PARAMETER_NOT_SET) {
4,930✔
1585
        this.undeclareParameter(parameter.name);
8✔
1586
        deletedParameters.push(parameter);
8✔
1587
      } else {
1588
        this._parameters.set(parameter.name, parameter);
4,922✔
1589
        if (declareParameterMode) {
4,922✔
1590
          newParameters.push(parameter);
4,890✔
1591
        } else {
1592
          changedParameters.push(parameter);
32✔
1593
        }
1594
      }
1595
    }
1596

1597
    // create ParameterEvent
1598
    const parameterEvent = new (loader.loadInterface(
4,930✔
1599
      PARAMETER_EVENT_MSG_TYPE
1600
    ))();
1601

1602
    const { seconds, nanoseconds } = this._clock.now().secondsAndNanoseconds;
4,930✔
1603
    parameterEvent.stamp = {
4,930✔
1604
      sec: Number(seconds),
1605
      nanosec: Number(nanoseconds),
1606
    };
1607

1608
    parameterEvent.node =
4,930✔
1609
      this.namespace() === '/'
4,930✔
1610
        ? this.namespace() + this.name()
1611
        : this.namespace() + '/' + this.name();
1612

1613
    if (newParameters.length > 0) {
4,930✔
1614
      parameterEvent['new_parameters'] = newParameters.map((parameter) =>
4,890✔
1615
        parameter.toParameterMessage()
4,890✔
1616
      );
1617
    }
1618
    if (changedParameters.length > 0) {
4,930✔
1619
      parameterEvent['changed_parameters'] = changedParameters.map(
32✔
1620
        (parameter) => parameter.toParameterMessage()
32✔
1621
      );
1622
    }
1623
    if (deletedParameters.length > 0) {
4,930✔
1624
      parameterEvent['deleted_parameters'] = deletedParameters.map(
8✔
1625
        (parameter) => parameter.toParameterMessage()
8✔
1626
      );
1627
    }
1628

1629
    // Publish ParameterEvent.
1630
    this._parameterEventPublisher.publish(parameterEvent);
4,930✔
1631

1632
    return {
4,930✔
1633
      successful: true,
1634
      reason: '',
1635
    };
1636
  }
1637

1638
  /**
1639
   * This callback is called when declaring a parameter or setting a parameter.
1640
   * The callback is provided a list of parameters and returns a SetParameterResult
1641
   * to indicate approval or veto of the operation.
1642
   *
1643
   * @callback SetParametersCallback
1644
   * @param {Parameter[]} parameters - The message published
1645
   * @returns {rcl_interfaces.msg.SetParameterResult} -
1646
   *
1647
   * @see [Node.addOnSetParametersCallback]{@link Node#addOnSetParametersCallback}
1648
   * @see [Node.removeOnSetParametersCallback]{@link Node#removeOnSetParametersCallback}
1649
   */
1650

1651
  /**
1652
   * Add a callback to the front of the list of callbacks invoked for parameter declaration
1653
   * and setting. No checks are made for duplicate callbacks.
1654
   *
1655
   * @param {SetParametersCallback} callback - The callback to add.
1656
   * @returns {undefined}
1657
   */
1658
  addOnSetParametersCallback(callback) {
1659
    this._setParametersCallbacks.unshift(callback);
4,674✔
1660
  }
1661

1662
  /**
1663
   * Remove a callback from the list of SetParametersCallbacks.
1664
   * If the callback is not found the process is a nop.
1665
   *
1666
   * @param {SetParametersCallback} callback - The callback to be removed
1667
   * @returns {undefined}
1668
   */
1669
  removeOnSetParametersCallback(callback) {
1670
    const idx = this._setParametersCallbacks.indexOf(callback);
16✔
1671
    if (idx > -1) {
16!
1672
      this._setParametersCallbacks.splice(idx, 1);
16✔
1673
    }
1674
  }
1675

1676
  /**
1677
   * Get the fully qualified name of the node.
1678
   *
1679
   * @returns {string} - String containing the fully qualified name of the node.
1680
   */
1681
  getFullyQualifiedName() {
1682
    return rclnodejs.getFullyQualifiedName(this.handle);
8✔
1683
  }
1684

1685
  /**
1686
   * Get the RMW implementation identifier
1687
   * @returns {string} - The RMW implementation identifier.
1688
   */
1689
  getRMWImplementationIdentifier() {
1690
    return rclnodejs.getRMWImplementationIdentifier();
8✔
1691
  }
1692

1693
  // returns on 1st error or result {successful, reason}
1694
  _validateParameters(parameters = [], declareParameterMode = false) {
×
1695
    for (const parameter of parameters) {
4,938✔
1696
      // detect invalid parameter
1697
      try {
4,938✔
1698
        parameter.validate();
4,938✔
1699
      } catch {
UNCOV
1700
        return {
×
1701
          successful: false,
1702
          reason: `Invalid ${parameter.name}`,
1703
        };
1704
      }
1705

1706
      // detect undeclared parameter
1707
      if (!this.hasParameterDescriptor(parameter.name)) {
4,938!
UNCOV
1708
        return {
×
1709
          successful: false,
1710
          reason: `Parameter ${parameter.name} has not been declared`,
1711
        };
1712
      }
1713

1714
      // detect readonly parameter that can not be updated
1715
      const descriptor = this.getParameterDescriptor(parameter.name);
4,938✔
1716
      if (!declareParameterMode && descriptor.readOnly) {
4,938!
UNCOV
1717
        return {
×
1718
          successful: false,
1719
          reason: `Parameter ${parameter.name} is readonly`,
1720
        };
1721
      }
1722

1723
      // validate parameter against descriptor if not an undeclare action
1724
      if (parameter.type != ParameterType.PARAMETER_NOT_SET) {
4,938✔
1725
        try {
4,930✔
1726
          descriptor.validateParameter(parameter);
4,930✔
1727
        } catch {
UNCOV
1728
          return {
×
1729
            successful: false,
1730
            reason: `Parameter ${parameter.name} does not  readonly`,
1731
          };
1732
        }
1733
      }
1734
    }
1735

1736
    return {
4,938✔
1737
      successful: true,
1738
      reason: null,
1739
    };
1740
  }
1741

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

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

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

1780
    // collect global CLI global parameters, name == /**
1781
    let paramOverrides = cliParamOverrides.get('/**'); // array of ParameterOverrides
4,610✔
1782
    if (paramOverrides) {
4,610✔
1783
      for (const parameter of paramOverrides) {
40✔
1784
        overrides.set(parameter.name, parameter);
48✔
1785
      }
1786
    }
1787

1788
    // merge CLI node parameterOverrides with global parameterOverrides, replace existing
1789
    paramOverrides = cliParamOverrides.get(this.name()); // array of ParameterOverrides
4,610✔
1790
    if (paramOverrides) {
4,610✔
1791
      for (const parameter of paramOverrides) {
40✔
1792
        overrides.set(parameter.name, parameter);
56✔
1793
      }
1794
    }
1795

1796
    return overrides;
4,610✔
1797
  }
1798

1799
  /**
1800
   * Invokes the callback with a raw message of the given type. After the callback completes
1801
   * the message will be destroyed.
1802
   * @param {function} Type - Message type to create.
1803
   * @param {function} callback - Callback to invoke. First parameter will be the raw message,
1804
   * and the second is a function to retrieve the deserialized message.
1805
   * @returns {undefined}
1806
   */
1807
  _runWithMessageType(Type, callback) {
1808
    let message = new Type();
5,558✔
1809

1810
    callback(message.toRawROS(), () => {
5,558✔
1811
      let result = new Type();
4,391✔
1812
      result.deserialize(message.refObject);
4,391✔
1813

1814
      return result;
4,391✔
1815
    });
1816

1817
    Type.destoryRawROS(message);
5,558✔
1818
  }
1819

1820
  _addActionClient(actionClient) {
1821
    this._actionClients.push(actionClient);
324✔
1822
    this.syncHandles();
324✔
1823
  }
1824

1825
  _addActionServer(actionServer) {
1826
    this._actionServers.push(actionServer);
324✔
1827
    this.syncHandles();
324✔
1828
  }
1829
}
1830

1831
/**
1832
 * Create an Options instance initialized with default values.
1833
 * @returns {Options} - The new initialized instance.
1834
 * @static
1835
 * @example
1836
 * {
1837
 *   enableTypedArray: true,
1838
 *   isRaw: false,
1839
 *   qos: QoS.profileDefault,
1840
 *   contentFilter: undefined,
1841
 * }
1842
 */
1843
Node.getDefaultOptions = function () {
208✔
1844
  return {
42,698✔
1845
    enableTypedArray: true,
1846
    isRaw: false,
1847
    qos: QoS.profileDefault,
1848
    contentFilter: undefined,
1849
  };
1850
};
1851

1852
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