rclnodejs 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/binding.gyp +2 -0
  2. package/index.js +152 -0
  3. package/lib/action/client.js +109 -10
  4. package/lib/action/deferred.js +8 -2
  5. package/lib/action/server.js +10 -1
  6. package/lib/action/uuid.js +4 -1
  7. package/lib/client.js +218 -4
  8. package/lib/clock.js +182 -1
  9. package/lib/clock_change.js +49 -0
  10. package/lib/clock_event.js +88 -0
  11. package/lib/context.js +12 -2
  12. package/lib/duration.js +37 -12
  13. package/lib/errors.js +621 -0
  14. package/lib/event_handler.js +21 -4
  15. package/lib/interface_loader.js +52 -12
  16. package/lib/lifecycle.js +8 -2
  17. package/lib/logging.js +90 -3
  18. package/lib/message_introspector.js +123 -0
  19. package/lib/message_serialization.js +10 -2
  20. package/lib/message_validation.js +512 -0
  21. package/lib/native_loader.js +9 -4
  22. package/lib/node.js +403 -50
  23. package/lib/node_options.js +40 -1
  24. package/lib/observable_subscription.js +105 -0
  25. package/lib/parameter.js +172 -35
  26. package/lib/parameter_client.js +506 -0
  27. package/lib/parameter_watcher.js +309 -0
  28. package/lib/publisher.js +56 -1
  29. package/lib/qos.js +79 -5
  30. package/lib/rate.js +6 -1
  31. package/lib/serialization.js +7 -2
  32. package/lib/subscription.js +8 -0
  33. package/lib/time.js +136 -21
  34. package/lib/time_source.js +13 -4
  35. package/lib/timer.js +42 -0
  36. package/lib/utils.js +27 -1
  37. package/lib/validator.js +74 -19
  38. package/package.json +4 -2
  39. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  40. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  41. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  42. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  43. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  44. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  45. package/rosidl_gen/message_translator.js +0 -61
  46. package/scripts/config.js +1 -0
  47. package/src/addon.cpp +2 -0
  48. package/src/clock_event.cpp +268 -0
  49. package/src/clock_event.hpp +62 -0
  50. package/src/macros.h +2 -4
  51. package/src/rcl_action_server_bindings.cpp +21 -3
  52. package/src/rcl_bindings.cpp +59 -0
  53. package/src/rcl_context_bindings.cpp +5 -0
  54. package/src/rcl_graph_bindings.cpp +73 -0
  55. package/src/rcl_logging_bindings.cpp +158 -0
  56. package/src/rcl_node_bindings.cpp +14 -2
  57. package/src/rcl_publisher_bindings.cpp +12 -0
  58. package/src/rcl_service_bindings.cpp +7 -6
  59. package/src/rcl_subscription_bindings.cpp +51 -14
  60. package/src/rcl_time_point_bindings.cpp +135 -0
  61. package/src/rcl_timer_bindings.cpp +140 -0
  62. package/src/rcl_utilities.cpp +103 -2
  63. package/src/rcl_utilities.h +7 -1
  64. package/types/action_client.d.ts +27 -2
  65. package/types/base.d.ts +6 -0
  66. package/types/client.d.ts +65 -1
  67. package/types/clock.d.ts +86 -0
  68. package/types/clock_change.d.ts +27 -0
  69. package/types/clock_event.d.ts +51 -0
  70. package/types/errors.d.ts +496 -0
  71. package/types/index.d.ts +10 -0
  72. package/types/logging.d.ts +32 -0
  73. package/types/message_introspector.d.ts +75 -0
  74. package/types/message_validation.d.ts +183 -0
  75. package/types/node.d.ts +107 -0
  76. package/types/node_options.d.ts +13 -0
  77. package/types/observable_subscription.d.ts +39 -0
  78. package/types/parameter_client.d.ts +252 -0
  79. package/types/parameter_watcher.d.ts +104 -0
  80. package/types/publisher.d.ts +28 -1
  81. package/types/qos.d.ts +18 -0
  82. package/types/subscription.d.ts +6 -0
  83. package/types/timer.d.ts +18 -0
  84. package/types/validator.d.ts +86 -0
@@ -27,18 +27,24 @@ class NodeOptions {
27
27
  * @param {array} [parameterOverrides=[]]
28
28
  * @param {boolean} [automaticallyDeclareParametersFromOverrides=false]
29
29
  * @param {boolean} [startTypeDescriptionService=true]
30
+ * @param {boolean} [enableRosout=true]
31
+ * @param {QoS} [rosoutQos=QoS.profileDefault]
30
32
  */
31
33
  constructor(
32
34
  startParameterServices = true,
33
35
  parameterOverrides = [],
34
36
  automaticallyDeclareParametersFromOverrides = false,
35
- startTypeDescriptionService = true
37
+ startTypeDescriptionService = true,
38
+ enableRosout = true,
39
+ rosoutQos = null
36
40
  ) {
37
41
  this._startParameterServices = startParameterServices;
38
42
  this._parameterOverrides = parameterOverrides;
39
43
  this._automaticallyDeclareParametersFromOverrides =
40
44
  automaticallyDeclareParametersFromOverrides;
41
45
  this._startTypeDescriptionService = startTypeDescriptionService;
46
+ this._enableRosout = enableRosout;
47
+ this._rosoutQos = rosoutQos;
42
48
  }
43
49
 
44
50
  /**
@@ -125,6 +131,39 @@ class NodeOptions {
125
131
  this._startTypeDescriptionService = willStartTypeDescriptionService;
126
132
  }
127
133
 
134
+ /**
135
+ * Get the enableRosout option.
136
+ * Default value = true;
137
+ * @returns {boolean} - true if the rosout logging is enabled.
138
+ */
139
+ get enableRosout() {
140
+ return this._enableRosout;
141
+ }
142
+
143
+ /**
144
+ * Set enableRosout.
145
+ * @param {boolean} enableRosout
146
+ */
147
+ set enableRosout(enableRosout) {
148
+ this._enableRosout = enableRosout;
149
+ }
150
+
151
+ /**
152
+ * Get the rosoutQos option.
153
+ * @returns {QoS} - The QoS profile for rosout.
154
+ */
155
+ get rosoutQos() {
156
+ return this._rosoutQos;
157
+ }
158
+
159
+ /**
160
+ * Set rosoutQos.
161
+ * @param {QoS} rosoutQos
162
+ */
163
+ set rosoutQos(rosoutQos) {
164
+ this._rosoutQos = rosoutQos;
165
+ }
166
+
128
167
  /**
129
168
  * Return an instance configured with default options.
130
169
  * @returns {NodeOptions} - An instance with default values.
@@ -0,0 +1,105 @@
1
+ // Copyright (c) 2025 Mahmoud Alghalayini. 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 { Subject } = require('rxjs');
18
+
19
+ /**
20
+ * A wrapper that provides RxJS Observable support for ROS 2 subscriptions.
21
+ * This class wraps a standard Subscription and emits messages through an Observable.
22
+ *
23
+ * @class ObservableSubscription
24
+ * @hideconstructor
25
+ */
26
+ class ObservableSubscription {
27
+ #subscription;
28
+ #subject;
29
+ #destroyed;
30
+
31
+ /**
32
+ * Create an ObservableSubscription wrapper.
33
+ * @param {Subscription} subscription - The underlying ROS 2 subscription
34
+ */
35
+ constructor(subscription) {
36
+ this.#subscription = subscription;
37
+ this.#subject = new Subject();
38
+ this.#destroyed = false;
39
+ }
40
+
41
+ /**
42
+ * Get the RxJS Observable for this subscription.
43
+ * Use this to pipe operators and subscribe to messages.
44
+ * @type {Observable}
45
+ */
46
+ get observable() {
47
+ return this.#subject.asObservable();
48
+ }
49
+
50
+ /**
51
+ * Get the underlying ROS 2 subscription.
52
+ * @type {Subscription}
53
+ */
54
+ get subscription() {
55
+ return this.#subscription;
56
+ }
57
+
58
+ /**
59
+ * Get the topic name.
60
+ * @type {string}
61
+ */
62
+ get topic() {
63
+ return this.#subscription.topic;
64
+ }
65
+
66
+ /**
67
+ * Check if this observable subscription has been destroyed.
68
+ * @type {boolean}
69
+ */
70
+ get isDestroyed() {
71
+ return this.#destroyed;
72
+ }
73
+
74
+ /**
75
+ * Internal method to emit a message to subscribers.
76
+ * Called by the subscription's processResponse.
77
+ * @private
78
+ * @param {any} message - The message to emit
79
+ */
80
+ _emit(message) {
81
+ if (!this.#destroyed) {
82
+ this.#subject.next(message);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Complete the observable and clean up resources.
88
+ * After calling this, no more messages will be emitted.
89
+ */
90
+ complete() {
91
+ if (!this.#destroyed) {
92
+ this.#destroyed = true;
93
+ this.#subject.complete();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Alias for complete() for consistency with RxJS naming.
99
+ */
100
+ destroy() {
101
+ this.complete();
102
+ }
103
+ }
104
+
105
+ module.exports = ObservableSubscription;
package/lib/parameter.js CHANGED
@@ -20,6 +20,12 @@
20
20
  'use strict';
21
21
 
22
22
  const { isClose } = require('./utils.js');
23
+ const {
24
+ TypeValidationError,
25
+ RangeValidationError,
26
+ ParameterError,
27
+ ParameterTypeError,
28
+ } = require('./errors.js');
23
29
 
24
30
  /**
25
31
  * The plus/minus tolerance for determining number equivalence.
@@ -184,17 +190,25 @@ class Parameter {
184
190
  typeof this.name !== 'string' ||
185
191
  this.name.trim().length === 0
186
192
  ) {
187
- throw new TypeError('Invalid name');
193
+ throw new TypeValidationError('name', this.name, 'non-empty string', {
194
+ entityType: 'parameter',
195
+ });
188
196
  }
189
197
 
190
198
  // validate type
191
199
  if (!validType(this.type)) {
192
- throw new TypeError('Invalid type');
200
+ throw new ParameterError('Invalid parameter type', this.name, {
201
+ details: { providedType: this.type },
202
+ });
193
203
  }
194
204
 
195
205
  // validate value
196
206
  if (!validValue(this.value, this.type)) {
197
- throw new TypeError('Incompatible value.');
207
+ throw new ParameterTypeError(this.name, this.type, typeof this.value, {
208
+ details: {
209
+ providedValue: this.value,
210
+ },
211
+ });
198
212
  }
199
213
 
200
214
  this._dirty = false;
@@ -383,10 +397,23 @@ class ParameterDescriptor {
383
397
  return;
384
398
  }
385
399
  if (!(range instanceof Range)) {
386
- throw TypeError('Expected instance of Range.');
400
+ throw new TypeValidationError('range', range, 'Range', {
401
+ entityType: 'parameter descriptor',
402
+ parameterName: this.name,
403
+ });
387
404
  }
388
405
  if (!range.isValidType(this.type)) {
389
- throw TypeError('Incompatible Range');
406
+ throw new ParameterError(
407
+ 'Incompatible Range for parameter type',
408
+ this.name,
409
+ {
410
+ entityType: 'parameter descriptor',
411
+ details: {
412
+ rangeType: range.constructor.name,
413
+ parameterType: this.type,
414
+ },
415
+ }
416
+ );
390
417
  }
391
418
 
392
419
  this._range = range;
@@ -405,22 +432,40 @@ class ParameterDescriptor {
405
432
  typeof this.name !== 'string' ||
406
433
  this.name.trim().length === 0
407
434
  ) {
408
- throw new TypeError('Invalid name');
435
+ throw new TypeValidationError('name', this.name, 'non-empty string', {
436
+ entityType: 'parameter descriptor',
437
+ });
409
438
  }
410
439
 
411
440
  // validate type
412
441
  if (!validType(this.type)) {
413
- throw new TypeError('Invalid type');
442
+ throw new ParameterError('Invalid parameter type', this.name, {
443
+ entityType: 'parameter descriptor',
444
+ details: { providedType: this.type },
445
+ });
414
446
  }
415
447
 
416
448
  // validate description
417
449
  if (this.description && typeof this.description !== 'string') {
418
- throw new TypeError('Invalid description');
450
+ throw new TypeValidationError('description', this.description, 'string', {
451
+ entityType: 'parameter descriptor',
452
+ parameterName: this.name,
453
+ });
419
454
  }
420
455
 
421
456
  // validate rangeConstraint
422
457
  if (this.hasRange() && !this.range.isValidType(this.type)) {
423
- throw new TypeError('Incompatible Range');
458
+ throw new ParameterError(
459
+ 'Incompatible Range for parameter type',
460
+ this.name,
461
+ {
462
+ entityType: 'parameter descriptor',
463
+ details: {
464
+ rangeType: this.range.constructor.name,
465
+ parameterType: this.type,
466
+ },
467
+ }
468
+ );
424
469
  }
425
470
  }
426
471
 
@@ -433,27 +478,66 @@ class ParameterDescriptor {
433
478
  */
434
479
  validateParameter(parameter) {
435
480
  if (!parameter) {
436
- throw new TypeError('Parameter is undefined');
481
+ throw new TypeValidationError('parameter', parameter, 'Parameter', {
482
+ entityType: 'parameter descriptor',
483
+ parameterName: this.name,
484
+ });
437
485
  }
438
486
 
439
487
  // ensure parameter is valid
440
488
  try {
441
489
  parameter.validate();
442
- } catch {
443
- throw new TypeError('Parameter is invalid');
490
+ } catch (err) {
491
+ throw new ParameterError('Parameter is invalid', parameter.name, {
492
+ cause: err,
493
+ details: { validationError: err.message },
494
+ });
444
495
  }
445
496
 
446
497
  // ensure this descriptor is valid
447
498
  try {
448
499
  this.validate();
449
- } catch {
450
- throw new Error('Descriptor is invalid.');
500
+ } catch (err) {
501
+ throw new ParameterError('Descriptor is invalid', this.name, {
502
+ entityType: 'parameter descriptor',
503
+ cause: err,
504
+ details: { validationError: err.message },
505
+ });
451
506
  }
452
507
 
453
- if (this.name !== parameter.name) throw new Error('Name mismatch');
454
- if (this.type !== parameter.type) throw new Error('Type mismatch');
508
+ if (this.name !== parameter.name) {
509
+ throw new ParameterError('Name mismatch', this.name, {
510
+ details: {
511
+ descriptorName: this.name,
512
+ parameterName: parameter.name,
513
+ },
514
+ });
515
+ }
516
+ if (this.type !== parameter.type) {
517
+ throw new ParameterTypeError(this.name, this.type, parameter.type, {
518
+ details: {
519
+ expectedType: this.type,
520
+ actualType: parameter.type,
521
+ },
522
+ });
523
+ }
455
524
  if (this.hasRange() && !this.range.inRange(parameter.value)) {
456
- throw new RangeError('Parameter value is not in descriptor range');
525
+ throw new RangeValidationError(
526
+ 'value',
527
+ parameter.value,
528
+ `${this.range.fromValue} to ${this.range.toValue}`,
529
+ {
530
+ entityType: 'parameter',
531
+ parameterName: parameter.name,
532
+ details: {
533
+ range: {
534
+ from: this.range.fromValue,
535
+ to: this.range.toValue,
536
+ step: this.range.step,
537
+ },
538
+ },
539
+ }
540
+ );
457
541
  }
458
542
  }
459
543
 
@@ -550,7 +634,9 @@ class Range {
550
634
  true
551
635
  );
552
636
  } else if (typeof value !== 'number' && typeof value !== 'bigint') {
553
- throw new TypeError('Value must be a number or bigint');
637
+ throw new TypeValidationError('value', value, 'number or bigint', {
638
+ entityType: 'range',
639
+ });
554
640
  }
555
641
 
556
642
  return true;
@@ -693,30 +779,80 @@ class IntegerRange extends Range {
693
779
  * @param {any} value - The value to infer it's ParameterType
694
780
  * @returns {ParameterType} - The ParameterType that best scribes the value.
695
781
  */
696
- // eslint-disable-next-line no-unused-vars
697
782
  function parameterTypeFromValue(value) {
698
- if (!value) return ParameterType.PARAMETER_NOT_SET;
699
- if (typeof value === 'boolean') return ParameterType.PARAMETER_BOOL;
700
- if (typeof value === 'string') return ParameterType.PARAMETER_STRING;
701
- if (typeof value === 'number') return ParameterType.PARAMETER_DOUBLE;
702
- if (Array.isArray(value)) {
783
+ if (value === null || value === undefined) {
784
+ return ParameterType.PARAMETER_NOT_SET;
785
+ }
786
+
787
+ if (typeof value === 'boolean') {
788
+ return ParameterType.PARAMETER_BOOL;
789
+ }
790
+
791
+ if (typeof value === 'string') {
792
+ return ParameterType.PARAMETER_STRING;
793
+ }
794
+
795
+ if (typeof value === 'bigint') {
796
+ return ParameterType.PARAMETER_INTEGER;
797
+ }
798
+
799
+ if (typeof value === 'number') {
800
+ // Distinguish between integer and double
801
+ return Number.isInteger(value)
802
+ ? ParameterType.PARAMETER_INTEGER
803
+ : ParameterType.PARAMETER_DOUBLE;
804
+ }
805
+
806
+ // Handle TypedArrays
807
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
808
+ if (value instanceof Uint8Array) {
809
+ return ParameterType.PARAMETER_BYTE_ARRAY;
810
+ }
811
+ // For other typed arrays, infer from first element
703
812
  if (value.length > 0) {
704
- const elementType = parameterTypeFromValue(value[0]);
705
- switch (elementType) {
706
- case ParameterType.PARAMETER_BOOL:
707
- return ParameterType.PARAMETER_BOOL_ARRAY;
708
- case ParameterType.PARAMETER_DOUBLE:
709
- return ParameterType.PARAMETER_DOUBLE_ARRAY;
710
- case ParameterType.PARAMETER_STRING:
711
- return ParameterType.PARAMETER_STRING_ARRAY;
813
+ const firstType = parameterTypeFromValue(value[0]);
814
+ if (firstType === ParameterType.PARAMETER_INTEGER) {
815
+ return ParameterType.PARAMETER_INTEGER_ARRAY;
712
816
  }
817
+ return ParameterType.PARAMETER_DOUBLE_ARRAY;
713
818
  }
714
-
715
819
  return ParameterType.PARAMETER_NOT_SET;
716
820
  }
717
821
 
718
- // otherwise unrecognized value
719
- throw new TypeError('Unrecognized parameter type.');
822
+ if (Array.isArray(value)) {
823
+ if (value.length === 0) {
824
+ return ParameterType.PARAMETER_NOT_SET;
825
+ }
826
+
827
+ const elementType = parameterTypeFromValue(value[0]);
828
+ switch (elementType) {
829
+ case ParameterType.PARAMETER_BOOL:
830
+ return ParameterType.PARAMETER_BOOL_ARRAY;
831
+ case ParameterType.PARAMETER_INTEGER:
832
+ return ParameterType.PARAMETER_INTEGER_ARRAY;
833
+ case ParameterType.PARAMETER_DOUBLE:
834
+ return ParameterType.PARAMETER_DOUBLE_ARRAY;
835
+ case ParameterType.PARAMETER_STRING:
836
+ return ParameterType.PARAMETER_STRING_ARRAY;
837
+ default:
838
+ return ParameterType.PARAMETER_NOT_SET;
839
+ }
840
+ }
841
+
842
+ // Unrecognized value type
843
+ throw new TypeValidationError('value', value, 'valid parameter type', {
844
+ entityType: 'parameter',
845
+ details: {
846
+ supportedTypes: [
847
+ 'boolean',
848
+ 'string',
849
+ 'number',
850
+ 'boolean[]',
851
+ 'number[]',
852
+ 'string[]',
853
+ ],
854
+ },
855
+ });
720
856
  }
721
857
 
722
858
  /**
@@ -826,4 +962,5 @@ module.exports = {
826
962
  FloatingPointRange,
827
963
  IntegerRange,
828
964
  DEFAULT_NUMERIC_RANGE_TOLERANCE,
965
+ parameterTypeFromValue,
829
966
  };