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

RobotWebTools / rclnodejs / 15486350269

06 Jun 2025 08:32AM UTC coverage: 84.625% (-0.2%) from 84.784%
15486350269

push

github

minggangw
Implement event handler (#1159)

This PR adds support for subscription and publisher event handlers across native bindings and the JavaScript API, and updates tests and type definitions accordingly.

- Introduce C++ bindings and event handle management in `HandleManager` and `Executor`
- Extend `Node`, `Subscription`, and `Publisher` classes to accept and clean up event callbacks
- Add and adjust tests and type declarations for event handler functionality

Fix: #1142, #492

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%)

3 existing lines in 1 file now uncovered.

1881 of 2133 relevant lines covered (88.19%)

2784.97 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,870✔
167
      handles.includes(timer.handle)
31,557✔
168
    );
169
    let guardsReady = this._guards.filter((guard) =>
36,870✔
170
      handles.includes(guard.handle)
24✔
171
    );
172
    let subscriptionsReady = this._subscriptions.filter((subscription) =>
36,870✔
173
      handles.includes(subscription.handle)
3,775✔
174
    );
175
    let clientsReady = this._clients.filter((client) =>
36,870✔
176
      handles.includes(client.handle)
747✔
177
    );
178
    let servicesReady = this._services.filter((service) =>
36,870✔
179
      handles.includes(service.handle)
107,047✔
180
    );
181
    let actionClientsReady = this._actionClients.filter((actionClient) =>
36,870✔
182
      handles.includes(actionClient.handle)
1,246✔
183
    );
184
    let actionServersReady = this._actionServers.filter((actionServer) =>
36,870✔
185
      handles.includes(actionServer.handle)
1,246✔
186
    );
187
    let eventsReady = this._events.filter((event) =>
36,870✔
188
      handles.includes(event.handle)
24✔
189
    );
190

191
    timersReady.forEach((timer) => {
36,870✔
192
      if (timer.isReady()) {
31,475✔
193
        rclnodejs.callTimer(timer.handle);
31,089✔
194
        timer.callback();
31,089✔
195
      }
196
    });
197

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

202
    for (const subscription of subscriptionsReady) {
36,870✔
203
      if (subscription.isDestroyed()) continue;
3,483✔
204
      if (subscription.isRaw) {
3,402✔
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,394✔
213
        subscription.typeClass,
214
        (message, deserialize) => {
215
          let success = rclnodejs.rclTake(subscription.handle, message);
3,394✔
216
          if (success) {
3,394✔
217
            subscription.processResponse(deserialize());
2,825✔
218
          }
219
        }
220
      );
221
    }
222

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

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

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

245
    for (const service of servicesReady) {
36,870✔
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,870✔
263
      if (actionClient.isDestroyed()) continue;
587!
264

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

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

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

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

312
      if (properties.isFeedbackReady) {
587✔
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) {
587✔
328
        this._runWithMessageType(
314✔
329
          actionClient.typeClass.impl.GoalStatusArray,
330
          (message, deserialize) => {
331
            let success = rclnodejs.actionTakeStatus(
314✔
332
              actionClient.handle,
333
              message
334
            );
335
            if (success) {
314✔
336
              actionClient.processStatusMessage(deserialize());
268✔
337
            }
338
          }
339
        );
340
      }
341
    }
342

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

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

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

363
      if (properties.isCancelRequestReady) {
735✔
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) {
735✔
379
        this._runWithMessageType(
184✔
380
          actionServer.typeClass.impl.GetResultService.Request,
381
          (message, deserialize) => {
382
            const result = rclnodejs.actionTakeResultRequest(
184✔
383
              actionServer.handle,
384
              message
385
            );
386
            if (result) {
184✔
387
              actionServer.processResultRequest(result, deserialize());
120✔
388
            }
389
          }
390
        );
391
      }
392

393
      if (properties.isGoalExpired) {
735✔
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,870✔
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
   * @param {PublisherEventCallbacks} eventCallbacks - The event callbacks for the publisher.
602
   * @return {Publisher} - An instance of Publisher.
603
   */
604
  createPublisher(typeClass, topic, options, eventCallbacks) {
605
    return this._createPublisher(
7,482✔
606
      typeClass,
607
      topic,
608
      options,
609
      Publisher,
610
      eventCallbacks
611
    );
612
  }
613

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

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

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

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

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

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

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

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

707
    return subscription;
2,713✔
708
  }
709

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

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

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

741
    return client;
356✔
742
  }
743

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

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

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

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

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

801
    return service;
27,620✔
802
  }
803

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

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

819
    return guard;
24✔
820
  }
821

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1386
    return params;
9,604✔
1387
  }
1388

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

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

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

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

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

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

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

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

1475
    return descriptors;
9,892✔
1476
  }
1477

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1790
    return overrides;
4,610✔
1791
  }
1792

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

1804
    callback(message.toRawROS(), () => {
5,551✔
1805
      let result = new Type();
4,389✔
1806
      result.deserialize(message.refObject);
4,389✔
1807

1808
      return result;
4,389✔
1809
    });
1810

1811
    Type.destoryRawROS(message);
5,551✔
1812
  }
1813

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

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

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

1846
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

© 2025 Coveralls, Inc