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 +2 -1
- package/lib/parameter.js +1 -1
- package/package.json +3 -3
- package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
- package/rosidl_gen/packages.js +6 -1
- package/src/rcl_bindings.cpp +1 -1
- package/src/rcl_lifecycle_bindings.cpp +24 -1
- package/test_data_integrity.js +108 -0
- package/test_repro_exact.js +57 -0
- package/test_repro_hz.js +86 -0
- package/test_repro_pub.js +36 -0
- package/test_repro_stress.js +83 -0
- package/test_repro_sub.js +64 -0
- package/test_xproc_data.js +64 -0
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rclnodejs",
|
|
3
|
-
"version": "1.8.
|
|
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": "^
|
|
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": "^
|
|
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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/rosidl_gen/packages.js
CHANGED
|
@@ -52,7 +52,12 @@ function getSubFolder(filePath, amentExecuted) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (amentExecuted) {
|
|
55
|
-
|
|
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.
|
package/src/rcl_bindings.cpp
CHANGED
|
@@ -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 >=
|
|
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);
|
package/test_repro_hz.js
ADDED
|
@@ -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);
|