rclnodejs 1.8.0 → 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>
@@ -175,4 +175,14 @@ function loadNativeAddon() {
175
175
  }
176
176
  }
177
177
 
178
- module.exports = loadNativeAddon();
178
+ const addon = loadNativeAddon();
179
+
180
+ // Export internal functions for testing purposes
181
+ if (process.env.NODE_ENV === 'test') {
182
+ addon.TestHelpers = {
183
+ customFallbackLoader,
184
+ loadNativeAddon,
185
+ };
186
+ }
187
+
188
+ module.exports = addon;
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/lib/time.js CHANGED
@@ -352,8 +352,8 @@ class Time {
352
352
  toMsg() {
353
353
  const secondsAndNanoseconds = this.secondsAndNanoseconds;
354
354
  return {
355
- sec: secondsAndNanoseconds.seconds,
356
- nanosec: secondsAndNanoseconds.nanoseconds,
355
+ sec: Number(secondsAndNanoseconds.seconds),
356
+ nanosec: Number(secondsAndNanoseconds.nanoseconds),
357
357
  };
358
358
  }
359
359
 
@@ -102,7 +102,7 @@ class TimeSource {
102
102
  * @return {undefined}
103
103
  */
104
104
  attachNode(node) {
105
- if ((!node) instanceof rclnodejs.ShadowNode) {
105
+ if (!(node instanceof rclnodejs.ShadowNode)) {
106
106
  throw new TypeValidationError('node', node, 'Node', {
107
107
  entityType: 'time source',
108
108
  });
package/lib/utils.js CHANGED
@@ -54,7 +54,7 @@ function ensureDirSync(dirPath) {
54
54
  */
55
55
  async function pathExists(filePath) {
56
56
  try {
57
- await fsPromises.access(filePath);
57
+ await fsPromises.stat(filePath);
58
58
  return true;
59
59
  } catch {
60
60
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rclnodejs",
3
- "version": "1.8.0",
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",
@@ -25,7 +25,7 @@
25
25
  "install": "node scripts/install.js",
26
26
  "postinstall": "npm run generate-messages",
27
27
  "docs": "cd docs && make",
28
- "test": "nyc node --expose-gc ./scripts/run_test.js && tsd",
28
+ "test": "nyc node --expose-gc ./scripts/run_test.js && tsd && npm install --no-save electron && node test/electron/run_test.js",
29
29
  "test-idl": "nyc node --expose-gc ./scripts/run_test.js --idl",
30
30
  "lint": "eslint && node ./scripts/cpplint.js",
31
31
  "format": "clang-format -i -style=file ./src/*.cpp ./src/*.h && npx --yes prettier --write \"{lib,rosidl_gen,rostsd_gen,rosidl_parser,types,example,test,scripts,benchmark,rostsd_gen}/**/*.{js,md,ts}\" ./*.{js,md,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,10 +56,10 @@
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
- "globals": "^16.0.0",
62
+ "globals": "^17.0.0",
63
63
  "husky": "^9.1.7",
64
64
  "jsdoc": "^4.0.4",
65
65
  "lint-staged": "^16.2.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.
@@ -223,7 +228,7 @@ async function findPackagesInDirectory(dir, useIDL) {
223
228
 
224
229
  // If there is a folder named 'share' under the root path, we consider that
225
230
  // the ament build tool has been executed and |amentExecuted| will be true.
226
- fs.access(path.join(dir, 'share'), (err) => {
231
+ fs.stat(path.join(dir, 'share'), (err) => {
227
232
  if (err) {
228
233
  amentExecuted = false;
229
234
  }
@@ -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();
@@ -38,9 +38,9 @@ Napi::Value RclTake(const Napi::CallbackInfo& info) {
38
38
  rcl_ret_t ret = rcl_take(subscription, msg_taken, nullptr, nullptr);
39
39
 
40
40
  if (ret != RCL_RET_OK && ret != RCL_RET_SUBSCRIPTION_TAKE_FAILED) {
41
+ std::string error_string = rcl_get_error_string().str;
41
42
  rcl_reset_error();
42
- Napi::Error::New(env, rcl_get_error_string().str)
43
- .ThrowAsJavaScriptException();
43
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
44
44
  return Napi::Boolean::New(env, false);
45
45
  }
46
46
 
@@ -99,7 +99,7 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
99
99
  for (int i = 0; i < argc; i++) {
100
100
  std::string arg = jsArgv.Get(i).As<Napi::String>().Utf8Value();
101
101
  int len = arg.length() + 1;
102
- argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char*)));
102
+ argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char)));
103
103
  snprintf(argv[i], len, "%s", arg.c_str());
104
104
  }
105
105
  }
@@ -109,9 +109,9 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
109
109
  expression.c_str(), argc, (const char**)argv, &subscription_ops);
110
110
 
111
111
  if (ret != RCL_RET_OK) {
112
+ std::string error_string = rcl_get_error_string().str;
112
113
  rcl_reset_error();
113
- Napi::Error::New(env, rcl_get_error_string().str)
114
- .ThrowAsJavaScriptException();
114
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
115
115
  }
116
116
 
117
117
  if (argc) {
@@ -120,6 +120,11 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
120
120
  }
121
121
  free(argv);
122
122
  }
123
+
124
+ if (ret != RCL_RET_OK) {
125
+ free(subscription);
126
+ return env.Undefined();
127
+ }
123
128
  }
124
129
  }
125
130
 
@@ -127,11 +132,15 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
127
132
  GetMessageTypeSupport(package_name, message_sub_folder, message_name);
128
133
 
129
134
  if (ts) {
130
- THROW_ERROR_IF_NOT_EQUAL(
131
- RCL_RET_OK,
132
- rcl_subscription_init(subscription, node, ts, topic.c_str(),
133
- &subscription_ops),
134
- rcl_get_error_string().str);
135
+ rcl_ret_t ret = rcl_subscription_init(subscription, node, ts, topic.c_str(),
136
+ &subscription_ops);
137
+ if (ret != RCL_RET_OK) {
138
+ std::string error_msg = rcl_get_error_string().str;
139
+ rcl_reset_error();
140
+ Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
141
+ free(subscription);
142
+ return env.Undefined();
143
+ }
135
144
 
136
145
  auto js_obj = RclHandle::NewInstance(
137
146
  env, subscription, node_handle, [node, env](void* ptr) {
@@ -139,14 +148,18 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
139
148
  reinterpret_cast<rcl_subscription_t*>(ptr);
140
149
  rcl_ret_t ret = rcl_subscription_fini(subscription, node);
141
150
  free(ptr);
142
- THROW_ERROR_IF_NOT_EQUAL_NO_RETURN(RCL_RET_OK, ret,
143
- rcl_get_error_string().str);
151
+ if (ret != RCL_RET_OK) {
152
+ std::string error_msg = rcl_get_error_string().str;
153
+ rcl_reset_error();
154
+ Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
155
+ }
144
156
  });
145
157
 
146
158
  return js_obj;
147
159
  } else {
148
160
  Napi::Error::New(env, GetErrorMessageAndClear())
149
161
  .ThrowAsJavaScriptException();
162
+ free(subscription);
150
163
  return env.Undefined();
151
164
  }
152
165
  }
@@ -235,7 +248,7 @@ Napi::Value SetContentFilter(const Napi::CallbackInfo& info) {
235
248
  for (int i = 0; i < argc; i++) {
236
249
  std::string arg = jsArgv.Get(i).As<Napi::String>().Utf8Value();
237
250
  int len = arg.length() + 1;
238
- argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char*)));
251
+ argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char)));
239
252
  snprintf(argv[i], len, "%s", arg.c_str());
240
253
  }
241
254
  }
@@ -245,15 +258,23 @@ Napi::Value SetContentFilter(const Napi::CallbackInfo& info) {
245
258
  rcl_subscription_content_filter_options_t options =
246
259
  rcl_get_zero_initialized_subscription_content_filter_options();
247
260
 
248
- THROW_ERROR_IF_NOT_EQUAL(
249
- RCL_RET_OK,
250
- rcl_subscription_content_filter_options_set(
251
- subscription, expression.c_str(), argc, (const char**)argv, &options),
252
- rcl_get_error_string().str);
261
+ rcl_ret_t ret = rcl_subscription_content_filter_options_set(
262
+ subscription, expression.c_str(), argc, (const char**)argv, &options);
263
+
264
+ if (ret != RCL_RET_OK) {
265
+ if (argc) {
266
+ for (int i = 0; i < argc; i++) {
267
+ free(argv[i]);
268
+ }
269
+ free(argv);
270
+ }
271
+ std::string error_string = rcl_get_error_string().str;
272
+ rcl_reset_error();
273
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
274
+ return env.Undefined();
275
+ }
253
276
 
254
- THROW_ERROR_IF_NOT_EQUAL(
255
- RCL_RET_OK, rcl_subscription_set_content_filter(subscription, &options),
256
- rcl_get_error_string().str);
277
+ ret = rcl_subscription_set_content_filter(subscription, &options);
257
278
 
258
279
  if (argc) {
259
280
  for (int i = 0; i < argc; i++) {
@@ -262,6 +283,27 @@ Napi::Value SetContentFilter(const Napi::CallbackInfo& info) {
262
283
  free(argv);
263
284
  }
264
285
 
286
+ std::string error_string = "";
287
+ if (ret != RCL_RET_OK) {
288
+ error_string = rcl_get_error_string().str;
289
+ rcl_reset_error();
290
+ }
291
+
292
+ rcl_ret_t fini_ret =
293
+ rcl_subscription_content_filter_options_fini(subscription, &options);
294
+
295
+ if (ret != RCL_RET_OK) {
296
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
297
+ return env.Undefined();
298
+ }
299
+
300
+ if (fini_ret != RCL_RET_OK) {
301
+ error_string = rcl_get_error_string().str;
302
+ rcl_reset_error();
303
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
304
+ return env.Undefined();
305
+ }
306
+
265
307
  return Napi::Boolean::New(env, true);
266
308
  }
267
309
 
@@ -277,15 +319,33 @@ Napi::Value ClearContentFilter(const Napi::CallbackInfo& info) {
277
319
  rcl_subscription_content_filter_options_t options =
278
320
  rcl_get_zero_initialized_subscription_content_filter_options();
279
321
 
280
- THROW_ERROR_IF_NOT_EQUAL(
281
- RCL_RET_OK,
282
- rcl_subscription_content_filter_options_init(
283
- subscription, "", 0, (const char**)nullptr, &options),
284
- rcl_get_error_string().str);
322
+ rcl_ret_t ret = rcl_subscription_content_filter_options_init(
323
+ subscription, "", 0, (const char**)nullptr, &options);
324
+
325
+ if (ret != RCL_RET_OK) {
326
+ std::string error_string = rcl_get_error_string().str;
327
+ rcl_reset_error();
328
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
329
+ return env.Undefined();
330
+ }
331
+
332
+ ret = rcl_subscription_set_content_filter(subscription, &options);
333
+ rcl_ret_t fini_ret =
334
+ rcl_subscription_content_filter_options_fini(subscription, &options);
335
+
336
+ if (ret != RCL_RET_OK) {
337
+ std::string error_string = rcl_get_error_string().str;
338
+ rcl_reset_error();
339
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
340
+ return env.Undefined();
341
+ }
285
342
 
286
- THROW_ERROR_IF_NOT_EQUAL(
287
- RCL_RET_OK, rcl_subscription_set_content_filter(subscription, &options),
288
- rcl_get_error_string().str);
343
+ if (fini_ret != RCL_RET_OK) {
344
+ std::string error_string = rcl_get_error_string().str;
345
+ rcl_reset_error();
346
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
347
+ return env.Undefined();
348
+ }
289
349
 
290
350
  return Napi::Boolean::New(env, true);
291
351
  }
@@ -303,9 +363,9 @@ Napi::Value GetContentFilter(const Napi::CallbackInfo& info) {
303
363
 
304
364
  rcl_ret_t ret = rcl_subscription_get_content_filter(subscription, &options);
305
365
  if (ret != RCL_RET_OK) {
306
- Napi::Error::New(env, rcl_get_error_string().str)
307
- .ThrowAsJavaScriptException();
366
+ std::string error_msg = rcl_get_error_string().str;
308
367
  rcl_reset_error();
368
+ Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
309
369
  return env.Undefined();
310
370
  }
311
371
 
@@ -331,9 +391,9 @@ Napi::Value GetContentFilter(const Napi::CallbackInfo& info) {
331
391
  rcl_ret_t fini_ret =
332
392
  rcl_subscription_content_filter_options_fini(subscription, &options);
333
393
  if (fini_ret != RCL_RET_OK) {
334
- Napi::Error::New(env, rcl_get_error_string().str)
335
- .ThrowAsJavaScriptException();
394
+ std::string error_msg = rcl_get_error_string().str;
336
395
  rcl_reset_error();
396
+ Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
337
397
  return env.Undefined();
338
398
  }
339
399
 
@@ -347,9 +407,12 @@ Napi::Value GetPublisherCount(const Napi::CallbackInfo& info) {
347
407
  RclHandle::Unwrap(info[0].As<Napi::Object>())->ptr());
348
408
 
349
409
  size_t count = 0;
350
- THROW_ERROR_IF_NOT_EQUAL(
351
- rcl_subscription_get_publisher_count(subscription, &count), RCL_RET_OK,
352
- rcl_get_error_string().str);
410
+ rcl_ret_t ret = rcl_subscription_get_publisher_count(subscription, &count);
411
+ if (ret != RCL_RET_OK) {
412
+ std::string error_msg = rcl_get_error_string().str;
413
+ rcl_reset_error();
414
+ Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
415
+ }
353
416
 
354
417
  return Napi::Number::New(env, count);
355
418
  }
@@ -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);