rclnodejs 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/lifecycle.js CHANGED
@@ -291,7 +291,8 @@ class LifecycleNode extends Node {
291
291
  // initialize native handle to rcl_lifecycle_state_machine
292
292
  this._stateMachineHandle = rclnodejs.createLifecycleStateMachine(
293
293
  this.handle,
294
- enableCommunicationInterface
294
+ enableCommunicationInterface,
295
+ this._clock.handle
295
296
  );
296
297
 
297
298
  // initialize Map<transitionId,TransitionCallback>
package/lib/parameter.js CHANGED
@@ -883,7 +883,7 @@ function validValue(value, type) {
883
883
  return type === ParameterType.PARAMETER_NOT_SET;
884
884
  }
885
885
 
886
- let result = true;
886
+ let result;
887
887
  switch (type) {
888
888
  case ParameterType.PARAMETER_NOT_SET:
889
889
  result = !value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rclnodejs",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "ROS2.0 JavaScript client with Node.js",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -48,7 +48,7 @@
48
48
  "url": "git+https://github.com/RobotWebTools/rclnodejs.git"
49
49
  },
50
50
  "devDependencies": {
51
- "@eslint/js": "^9.36.0",
51
+ "@eslint/js": "^10.0.1",
52
52
  "@types/node": "^25.0.2",
53
53
  "@typescript-eslint/eslint-plugin": "^8.18.0",
54
54
  "@typescript-eslint/parser": "^8.18.0",
@@ -56,7 +56,7 @@
56
56
  "commander": "^14.0.0",
57
57
  "coveralls": "^3.1.1",
58
58
  "deep-equal": "^2.2.3",
59
- "eslint": "^9.16.0",
59
+ "eslint": "^10.0.2",
60
60
  "eslint-config-prettier": "^10.0.2",
61
61
  "eslint-plugin-prettier": "^5.2.1",
62
62
  "globals": "^17.0.0",
@@ -52,7 +52,12 @@ function getSubFolder(filePath, amentExecuted) {
52
52
  }
53
53
 
54
54
  if (amentExecuted) {
55
- return filePath.match(/\w+\/share\/\w+\/(\w+)\//)[1];
55
+ const match = filePath.match(/\w+\/share\/\w+\/([\w-]+)\//);
56
+ if (match) {
57
+ // Handle non-standard subfolder names (e.g., msg-common, msg-ros2)
58
+ // by extracting only the base interface type before any hyphen.
59
+ return match[1].split('-')[0];
60
+ }
56
61
  }
57
62
  // If the |amentExecuted| equals to false, the file's extension will be assigned as
58
63
  // the name of sub folder.
@@ -14,7 +14,7 @@
14
14
 
15
15
  #include "rcl_bindings.h"
16
16
 
17
- #include <node.h>
17
+ #include <node_version.h>
18
18
  #include <rcl/arguments.h>
19
19
  #include <rcl/rcl.h>
20
20
 
@@ -71,7 +71,30 @@ Napi::Value CreateLifecycleStateMachine(const Napi::CallbackInfo& info) {
71
71
  const rosidl_service_type_support_t* gs =
72
72
  GetServiceTypeSupport("lifecycle_msgs", "GetState");
73
73
 
74
- #if ROS_VERSION >= 2105
74
+ #if ROS_VERSION >= 5000 // ROS2 Rolling
75
+ rcl_lifecycle_state_machine_options_t options =
76
+ rcl_lifecycle_get_default_state_machine_options();
77
+ options.enable_com_interface = info[1].As<Napi::Boolean>().Value();
78
+
79
+ RclHandle* clock_handle = RclHandle::Unwrap(info[2].As<Napi::Object>());
80
+ rcl_clock_t* clock = reinterpret_cast<rcl_clock_t*>(clock_handle->ptr());
81
+
82
+ THROW_ERROR_IF_NOT_EQUAL(
83
+ RCL_RET_OK,
84
+ rcl_lifecycle_state_machine_init(state_machine, node, clock, pn, cs, gs,
85
+ gas, gat, gtg, &options),
86
+ rcl_get_error_string().str);
87
+
88
+ auto js_obj = RclHandle::NewInstance(
89
+ env, state_machine, node_handle, [node, env](void* ptr) {
90
+ rcl_lifecycle_state_machine_t* state_machine =
91
+ reinterpret_cast<rcl_lifecycle_state_machine_t*>(ptr);
92
+ rcl_ret_t ret = rcl_lifecycle_state_machine_fini(state_machine, node);
93
+ free(ptr);
94
+ THROW_ERROR_IF_NOT_EQUAL_NO_RETURN(RCL_RET_OK, ret,
95
+ rcl_get_error_string().str);
96
+ });
97
+ #elif ROS_VERSION >= 2105
75
98
  rcl_lifecycle_state_machine_options_t options =
76
99
  rcl_lifecycle_get_default_state_machine_options();
77
100
  options.enable_com_interface = info[1].As<Napi::Boolean>().Value();
@@ -0,0 +1,108 @@
1
+ // Data integrity + throughput test for subscription
2
+ 'use strict';
3
+
4
+ const rclnodejs = require('./index.js');
5
+
6
+ const PUBLISH_HZ = 50;
7
+ const TEST_DURATION_SEC = 10;
8
+
9
+ async function main() {
10
+ await rclnodejs.init();
11
+
12
+ const pubNode = new rclnodejs.Node('data_pub_node');
13
+ const subNode = new rclnodejs.Node('data_sub_node');
14
+
15
+ const publisher = pubNode.createPublisher(
16
+ 'std_msgs/msg/Float64MultiArray',
17
+ '/data_integrity_topic'
18
+ );
19
+
20
+ let msgCount = 0;
21
+ let errCount = 0;
22
+ let lastTs = null;
23
+ const hzSamples = [];
24
+ const errors = [];
25
+
26
+ subNode.createSubscription(
27
+ 'std_msgs/msg/Float64MultiArray',
28
+ '/data_integrity_topic',
29
+ (msg) => {
30
+ const now = Date.now();
31
+ msgCount++;
32
+
33
+ // --- Data validation ---
34
+ // Each published message has data = [seqNo, seqNo*1.5, seqNo*2.5]
35
+ // Verify structure and values
36
+ if (!msg || !msg.data) {
37
+ errCount++;
38
+ errors.push(`msg#${msgCount}: missing data field, got: ${JSON.stringify(msg)}`);
39
+ } else if (!Array.isArray(msg.data) && !(msg.data instanceof Float64Array)) {
40
+ errCount++;
41
+ errors.push(`msg#${msgCount}: data is not array-like, type=${typeof msg.data}`);
42
+ } else if (msg.data.length !== 3) {
43
+ errCount++;
44
+ errors.push(`msg#${msgCount}: expected 3 elements, got ${msg.data.length}`);
45
+ } else {
46
+ const seqNo = msg.data[0];
47
+ const expectedB = seqNo * 1.5;
48
+ const expectedC = seqNo * 2.5;
49
+
50
+ if (Math.abs(msg.data[1] - expectedB) > 1e-9) {
51
+ errCount++;
52
+ errors.push(
53
+ `msg#${msgCount}: data[1] expected ${expectedB}, got ${msg.data[1]}`
54
+ );
55
+ }
56
+ if (Math.abs(msg.data[2] - expectedC) > 1e-9) {
57
+ errCount++;
58
+ errors.push(
59
+ `msg#${msgCount}: data[2] expected ${expectedC}, got ${msg.data[2]}`
60
+ );
61
+ }
62
+ }
63
+
64
+ if (lastTs) {
65
+ hzSamples.push(1000 / (now - lastTs));
66
+ }
67
+ lastTs = now;
68
+ }
69
+ );
70
+
71
+ pubNode.spin();
72
+ subNode.spin();
73
+
74
+ let pubSeq = 0;
75
+ const pubInterval = setInterval(() => {
76
+ pubSeq++;
77
+ publisher.publish({ data: [pubSeq, pubSeq * 1.5, pubSeq * 2.5] });
78
+ }, 1000 / PUBLISH_HZ);
79
+
80
+ setTimeout(() => {
81
+ clearInterval(pubInterval);
82
+
83
+ console.log(`\n=== Data Integrity Test Results ===`);
84
+ console.log(`Published: ${pubSeq} messages at ${PUBLISH_HZ} Hz`);
85
+ console.log(`Received: ${msgCount} messages`);
86
+ console.log(`Data errors: ${errCount}`);
87
+
88
+ if (errors.length > 0) {
89
+ console.log(`\nFirst 10 errors:`);
90
+ errors.slice(0, 10).forEach((e) => console.log(` ${e}`));
91
+ }
92
+
93
+ if (hzSamples.length > 0) {
94
+ const avgHz = hzSamples.reduce((a, b) => a + b, 0) / hzSamples.length;
95
+ console.log(`\nAvg Hz: ${avgHz.toFixed(2)}`);
96
+ }
97
+
98
+ const pass = errCount === 0 && msgCount > 0;
99
+ console.log(`\nResult: ${pass ? 'PASS - all data correct' : 'FAIL'}`);
100
+
101
+ pubNode.stop();
102
+ subNode.stop();
103
+ rclnodejs.shutdown();
104
+ process.exit(pass ? 0 : 1);
105
+ }, TEST_DURATION_SEC * 1000);
106
+ }
107
+
108
+ main().catch(console.error);
@@ -0,0 +1,57 @@
1
+ // Thorough latency/throughput test matching the exact issue scenario
2
+ 'use strict';
3
+
4
+ const rclnodejs = require('./index.js');
5
+
6
+ async function main() {
7
+ await rclnodejs.init();
8
+
9
+ const node = new rclnodejs.Node('test_node');
10
+
11
+ let lastTs;
12
+ let msgCount = 0;
13
+ const hzSamples = [];
14
+
15
+ node.createSubscription(
16
+ 'std_msgs/msg/Float64MultiArray',
17
+ '/map_to_base_link_pose2d',
18
+ (msg) => {
19
+ const now = Date.now();
20
+ msgCount++;
21
+ if (lastTs) {
22
+ const hz = 1000 / (now - lastTs);
23
+ hzSamples.push(hz);
24
+ console.log('Raw Hz:', hz.toFixed(2));
25
+ }
26
+ lastTs = now;
27
+ }
28
+ );
29
+
30
+ rclnodejs.spin(node);
31
+
32
+ console.log('Waiting for messages on /map_to_base_link_pose2d at ~10Hz...');
33
+ console.log('Run this in another terminal:');
34
+ console.log(
35
+ ' ros2 topic pub -r 10 /map_to_base_link_pose2d std_msgs/msg/Float64MultiArray "{data: [1.0, 2.0, 3.0]}"'
36
+ );
37
+
38
+ setTimeout(() => {
39
+ if (hzSamples.length > 0) {
40
+ const avgHz = hzSamples.reduce((a, b) => a + b, 0) / hzSamples.length;
41
+ console.log(`\n--- Summary ---`);
42
+ console.log(`Messages: ${msgCount}, Avg Hz: ${avgHz.toFixed(2)}`);
43
+ if (avgHz < 5) {
44
+ console.log('*** REGRESSION DETECTED ***');
45
+ } else {
46
+ console.log('Performance OK');
47
+ }
48
+ } else {
49
+ console.log('No messages received');
50
+ }
51
+ node.stop();
52
+ rclnodejs.shutdown();
53
+ process.exit(0);
54
+ }, 15000);
55
+ }
56
+
57
+ main().catch(console.error);
@@ -0,0 +1,86 @@
1
+ // Reprocer for https://github.com/RobotWebTools/rclnodejs/issues/1394
2
+ // Tests subscription throughput at ~10Hz publishing rate
3
+ 'use strict';
4
+
5
+ const rclnodejs = require('./index.js');
6
+
7
+ const PUBLISH_HZ = 10;
8
+ const TEST_DURATION_SEC = 10;
9
+
10
+ async function main() {
11
+ await rclnodejs.init();
12
+
13
+ const pubNode = new rclnodejs.Node('test_pub_node');
14
+ const subNode = new rclnodejs.Node('test_sub_node');
15
+
16
+ const publisher = pubNode.createPublisher(
17
+ 'std_msgs/msg/Float64MultiArray',
18
+ '/test_hz_topic'
19
+ );
20
+
21
+ let msgCount = 0;
22
+ let lastTs = null;
23
+ const hzSamples = [];
24
+
25
+ subNode.createSubscription(
26
+ 'std_msgs/msg/Float64MultiArray',
27
+ '/test_hz_topic',
28
+ (msg) => {
29
+ const now = Date.now();
30
+ msgCount++;
31
+ if (lastTs) {
32
+ const hz = 1000 / (now - lastTs);
33
+ hzSamples.push(hz);
34
+ }
35
+ lastTs = now;
36
+ }
37
+ );
38
+
39
+ pubNode.spin();
40
+ subNode.spin();
41
+
42
+ // Publish at target Hz
43
+ let pubCount = 0;
44
+ const pubInterval = setInterval(() => {
45
+ publisher.publish({ data: [1.0, 2.0, 3.0] });
46
+ pubCount++;
47
+ }, 1000 / PUBLISH_HZ);
48
+
49
+ // Wait for test duration then report
50
+ setTimeout(() => {
51
+ clearInterval(pubInterval);
52
+
53
+ if (hzSamples.length > 0) {
54
+ const avgHz =
55
+ hzSamples.reduce((a, b) => a + b, 0) / hzSamples.length;
56
+ const minHz = Math.min(...hzSamples);
57
+ const maxHz = Math.max(...hzSamples);
58
+
59
+ console.log(`Published: ${pubCount} messages`);
60
+ console.log(`Received: ${msgCount} messages`);
61
+ console.log(`Avg Hz: ${avgHz.toFixed(2)}`);
62
+ console.log(`Min Hz: ${minHz.toFixed(2)}`);
63
+ console.log(`Max Hz: ${maxHz.toFixed(2)}`);
64
+ console.log(
65
+ `Expected: ~${PUBLISH_HZ} Hz`
66
+ );
67
+
68
+ if (avgHz < PUBLISH_HZ * 0.5) {
69
+ console.log(
70
+ `\n*** REGRESSION DETECTED: Average Hz (${avgHz.toFixed(2)}) is less than 50% of expected (${PUBLISH_HZ}) ***`
71
+ );
72
+ } else {
73
+ console.log('\nPerformance looks OK.');
74
+ }
75
+ } else {
76
+ console.log('No messages received!');
77
+ }
78
+
79
+ pubNode.stop();
80
+ subNode.stop();
81
+ rclnodejs.shutdown();
82
+ process.exit(0);
83
+ }, TEST_DURATION_SEC * 1000);
84
+ }
85
+
86
+ main().catch(console.error);
@@ -0,0 +1,36 @@
1
+ // Publisher for repro test - runs in separate process
2
+ 'use strict';
3
+
4
+ const rclnodejs = require('./index.js');
5
+
6
+ const PUBLISH_HZ = parseInt(process.argv[2] || '100');
7
+
8
+ async function main() {
9
+ await rclnodejs.init();
10
+
11
+ const node = new rclnodejs.Node('test_publisher_node');
12
+ const publisher = node.createPublisher(
13
+ 'std_msgs/msg/Float64MultiArray',
14
+ '/test_hz_topic'
15
+ );
16
+
17
+ node.spin();
18
+
19
+ let pubCount = 0;
20
+ console.log(`Publishing at ${PUBLISH_HZ} Hz...`);
21
+
22
+ const pubInterval = setInterval(() => {
23
+ publisher.publish({ data: [1.0, 2.0, 3.0] });
24
+ pubCount++;
25
+ }, 1000 / PUBLISH_HZ);
26
+
27
+ setTimeout(() => {
28
+ clearInterval(pubInterval);
29
+ console.log(`Published ${pubCount} messages total`);
30
+ node.stop();
31
+ rclnodejs.shutdown();
32
+ process.exit(0);
33
+ }, 15000);
34
+ }
35
+
36
+ main().catch(console.error);
@@ -0,0 +1,83 @@
1
+ // Multi-frequency stress test for subscription performance
2
+ 'use strict';
3
+
4
+ const rclnodejs = require('./index.js');
5
+
6
+ const PUBLISH_HZ = parseInt(process.argv[2] || '100');
7
+ const TEST_DURATION_SEC = 10;
8
+
9
+ async function main() {
10
+ await rclnodejs.init();
11
+
12
+ const pubNode = new rclnodejs.Node('stress_pub_node');
13
+ const subNode = new rclnodejs.Node('stress_sub_node');
14
+
15
+ const publisher = pubNode.createPublisher(
16
+ 'std_msgs/msg/Float64MultiArray',
17
+ '/stress_test_topic'
18
+ );
19
+
20
+ let msgCount = 0;
21
+ let lastTs = null;
22
+ const hzSamples = [];
23
+
24
+ subNode.createSubscription(
25
+ 'std_msgs/msg/Float64MultiArray',
26
+ '/stress_test_topic',
27
+ (msg) => {
28
+ const now = Date.now();
29
+ msgCount++;
30
+ if (lastTs) {
31
+ const hz = 1000 / (now - lastTs);
32
+ hzSamples.push(hz);
33
+ }
34
+ lastTs = now;
35
+ }
36
+ );
37
+
38
+ pubNode.spin();
39
+ subNode.spin();
40
+
41
+ let pubCount = 0;
42
+
43
+ // Use high-resolution timer for more precise publishing
44
+ const intervalMs = 1000 / PUBLISH_HZ;
45
+ const pubInterval = setInterval(() => {
46
+ publisher.publish({ data: [1.0, 2.0, 3.0, 4.0, 5.0] });
47
+ pubCount++;
48
+ }, intervalMs);
49
+
50
+ setTimeout(() => {
51
+ clearInterval(pubInterval);
52
+
53
+ if (hzSamples.length > 0) {
54
+ const avgHz = hzSamples.reduce((a, b) => a + b, 0) / hzSamples.length;
55
+ const minHz = Math.min(...hzSamples);
56
+ const maxHz = Math.max(...hzSamples);
57
+ const dropRate = ((pubCount - msgCount) / pubCount * 100);
58
+
59
+ console.log(`Target: ${PUBLISH_HZ} Hz`);
60
+ console.log(`Published: ${pubCount}`);
61
+ console.log(`Received: ${msgCount}`);
62
+ console.log(`Avg Hz: ${avgHz.toFixed(2)}`);
63
+ console.log(`Min Hz: ${minHz.toFixed(2)}`);
64
+ console.log(`Max Hz: ${maxHz.toFixed(2)}`);
65
+ console.log(`Drop rate: ${dropRate.toFixed(1)}%`);
66
+
67
+ if (avgHz < PUBLISH_HZ * 0.5) {
68
+ console.log(`FAIL: Avg Hz significantly below target`);
69
+ } else {
70
+ console.log(`PASS`);
71
+ }
72
+ } else {
73
+ console.log('No messages received!');
74
+ }
75
+
76
+ pubNode.stop();
77
+ subNode.stop();
78
+ rclnodejs.shutdown();
79
+ process.exit(0);
80
+ }, TEST_DURATION_SEC * 1000);
81
+ }
82
+
83
+ main().catch(console.error);
@@ -0,0 +1,64 @@
1
+ // Subscriber for repro test - runs in separate process
2
+ 'use strict';
3
+
4
+ const rclnodejs = require('./index.js');
5
+
6
+ async function main() {
7
+ await rclnodejs.init();
8
+
9
+ const node = new rclnodejs.Node('test_subscriber_node');
10
+
11
+ let msgCount = 0;
12
+ let lastTs = null;
13
+ const hzSamples = [];
14
+ let startTime = null;
15
+
16
+ node.createSubscription(
17
+ 'std_msgs/msg/Float64MultiArray',
18
+ '/test_hz_topic',
19
+ (msg) => {
20
+ const now = Date.now();
21
+ msgCount++;
22
+ if (!startTime) startTime = now;
23
+ if (lastTs) {
24
+ const hz = 1000 / (now - lastTs);
25
+ hzSamples.push(hz);
26
+ if (msgCount % 50 === 0) {
27
+ const recentSamples = hzSamples.slice(-50);
28
+ const recentAvg =
29
+ recentSamples.reduce((a, b) => a + b, 0) / recentSamples.length;
30
+ console.log(
31
+ `msg#${msgCount} recent avg Hz: ${recentAvg.toFixed(2)}`
32
+ );
33
+ }
34
+ }
35
+ lastTs = now;
36
+ }
37
+ );
38
+
39
+ node.spin();
40
+
41
+ setTimeout(() => {
42
+ if (hzSamples.length > 0) {
43
+ const avgHz =
44
+ hzSamples.reduce((a, b) => a + b, 0) / hzSamples.length;
45
+ const minHz = Math.min(...hzSamples);
46
+ const maxHz = Math.max(...hzSamples);
47
+ const elapsed = (Date.now() - startTime) / 1000;
48
+
49
+ console.log(`\n--- Results ---`);
50
+ console.log(`Received: ${msgCount} messages in ${elapsed.toFixed(1)}s`);
51
+ console.log(`Avg Hz: ${avgHz.toFixed(2)}`);
52
+ console.log(`Min Hz: ${minHz.toFixed(2)}`);
53
+ console.log(`Max Hz: ${maxHz.toFixed(2)}`);
54
+ } else {
55
+ console.log('No messages received!');
56
+ }
57
+
58
+ node.stop();
59
+ rclnodejs.shutdown();
60
+ process.exit(0);
61
+ }, 12000);
62
+ }
63
+
64
+ main().catch(console.error);
@@ -0,0 +1,64 @@
1
+ // Cross-process data integrity test: validates data from external ROS2 publisher
2
+ 'use strict';
3
+
4
+ const rclnodejs = require('./index.js');
5
+
6
+ const EXPECTED_DATA = [1.0, 2.0, 3.0];
7
+
8
+ async function main() {
9
+ await rclnodejs.init();
10
+
11
+ const node = new rclnodejs.Node('xproc_data_sub');
12
+ let msgCount = 0;
13
+ let errCount = 0;
14
+ const errors = [];
15
+
16
+ node.createSubscription(
17
+ 'std_msgs/msg/Float64MultiArray',
18
+ '/xproc_data_topic',
19
+ (msg) => {
20
+ msgCount++;
21
+
22
+ if (!msg || !msg.data) {
23
+ errCount++;
24
+ errors.push(`msg#${msgCount}: missing data`);
25
+ } else {
26
+ const arr = Array.from(msg.data);
27
+ for (let i = 0; i < EXPECTED_DATA.length; i++) {
28
+ if (Math.abs(arr[i] - EXPECTED_DATA[i]) > 1e-9) {
29
+ errCount++;
30
+ errors.push(
31
+ `msg#${msgCount}: data[${i}] expected ${EXPECTED_DATA[i]}, got ${arr[i]}`
32
+ );
33
+ }
34
+ }
35
+ if (arr.length !== EXPECTED_DATA.length) {
36
+ errCount++;
37
+ errors.push(
38
+ `msg#${msgCount}: expected ${EXPECTED_DATA.length} elements, got ${arr.length}`
39
+ );
40
+ }
41
+ }
42
+ }
43
+ );
44
+
45
+ rclnodejs.spin(node);
46
+
47
+ console.log('Listening on /xproc_data_topic for 10s...');
48
+
49
+ setTimeout(() => {
50
+ console.log(`\n=== Cross-Process Data Integrity ===`);
51
+ console.log(`Received: ${msgCount} messages`);
52
+ console.log(`Data errors: ${errCount}`);
53
+ if (errors.length > 0) {
54
+ errors.slice(0, 10).forEach((e) => console.log(` ${e}`));
55
+ }
56
+ const pass = errCount === 0 && msgCount > 0;
57
+ console.log(`Result: ${pass ? 'PASS' : 'FAIL'}`);
58
+ node.stop();
59
+ rclnodejs.shutdown();
60
+ process.exit(0);
61
+ }, 10000);
62
+ }
63
+
64
+ main().catch(console.error);