rclnodejs 1.8.3 → 1.9.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 (90) hide show
  1. package/README.md +46 -37
  2. package/index.js +39 -0
  3. package/lib/action/client.js +61 -3
  4. package/lib/action/server_goal_handle.js +26 -1
  5. package/lib/message_info.js +94 -0
  6. package/lib/node.js +260 -13
  7. package/lib/parameter_event_handler.js +566 -0
  8. package/lib/parameter_watcher.js +12 -12
  9. package/lib/qos_overriding_options.js +358 -0
  10. package/lib/subscription.js +38 -5
  11. package/lib/timer.js +3 -2
  12. package/lib/wait_for_message.js +111 -0
  13. package/package.json +7 -4
  14. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  15. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  16. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  17. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  18. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  19. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  20. package/scripts/run_asan_test.sh +118 -0
  21. package/src/executor.cpp +36 -2
  22. package/src/executor.h +11 -0
  23. package/src/rcl_action_client_bindings.cpp +70 -1
  24. package/src/rcl_context_bindings.cpp +3 -3
  25. package/src/rcl_graph_bindings.cpp +2 -2
  26. package/src/rcl_subscription_bindings.cpp +70 -2
  27. package/src/rcl_timer_bindings.cpp +21 -2
  28. package/src/rcl_utilities.cpp +2 -2
  29. package/tools/jsdoc/Makefile +5 -0
  30. package/tools/jsdoc/README.md +96 -0
  31. package/tools/jsdoc/build-index.js +610 -0
  32. package/tools/jsdoc/publish.js +854 -0
  33. package/tools/jsdoc/regenerate-published-docs.js +605 -0
  34. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  35. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  36. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  37. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  38. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  39. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  40. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  41. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  42. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  43. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  44. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  45. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  46. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  47. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  48. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  49. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  50. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  51. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  52. package/tools/jsdoc/static/scripts/linenumber.js +25 -0
  53. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  54. package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
  55. package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
  56. package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
  57. package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
  58. package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
  59. package/tools/jsdoc/tmpl/augments.tmpl +10 -0
  60. package/tools/jsdoc/tmpl/container.tmpl +193 -0
  61. package/tools/jsdoc/tmpl/details.tmpl +143 -0
  62. package/tools/jsdoc/tmpl/example.tmpl +2 -0
  63. package/tools/jsdoc/tmpl/examples.tmpl +13 -0
  64. package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
  65. package/tools/jsdoc/tmpl/layout.tmpl +83 -0
  66. package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
  67. package/tools/jsdoc/tmpl/members.tmpl +43 -0
  68. package/tools/jsdoc/tmpl/method.tmpl +124 -0
  69. package/tools/jsdoc/tmpl/params.tmpl +133 -0
  70. package/tools/jsdoc/tmpl/properties.tmpl +110 -0
  71. package/tools/jsdoc/tmpl/returns.tmpl +12 -0
  72. package/tools/jsdoc/tmpl/source.tmpl +8 -0
  73. package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
  74. package/tools/jsdoc/tmpl/type.tmpl +7 -0
  75. package/types/action_client.d.ts +8 -0
  76. package/types/index.d.ts +34 -0
  77. package/types/message_info.d.ts +72 -0
  78. package/types/node.d.ts +90 -5
  79. package/types/parameter_event_handler.d.ts +150 -0
  80. package/types/qos.d.ts +55 -0
  81. package/types/subscription.d.ts +14 -2
  82. package/types/timer.d.ts +3 -2
  83. package/test_data_integrity.js +0 -108
  84. package/test_repro_exact.js +0 -57
  85. package/test_repro_hz.js +0 -86
  86. package/test_repro_pub.js +0 -36
  87. package/test_repro_stress.js +0 -83
  88. package/test_repro_sub.js +0 -64
  89. package/test_xproc_data.js +0 -64
  90. package/types/interfaces.d.ts +0 -8895
package/README.md CHANGED
@@ -1,27 +1,33 @@
1
- # rclnodejs [![Linux](https://github.com/RobotWebTools/rclnodejs/actions/workflows/linux-x64-push-test.yml/badge.svg?branch=develop)](https://github.com/RobotWebTools/rclnodejs/actions/workflows/linux-x64-push-test.yml?query=branch%3Adevelop)[![Linux](https://github.com/RobotWebTools/rclnodejs/actions/workflows/linux-arm64-push-test.yml/badge.svg?branch=develop)](https://github.com/RobotWebTools/rclnodejs/actions/workflows/linux-arm64-push-test.yml?query=branch%3Adevelop)
1
+ # rclnodejs
2
2
 
3
- `rclnodejs` is a Node.js client for the Robot Operating System (ROS 2). It provides a simple and easy JavaScript API for ROS 2 programming. TypeScript declarations are included to support use of rclnodejs in TypeScript projects.
3
+ `rclnodejs` is a Node.js client library for ROS 2 that provides JavaScript and TypeScript APIs for building ROS 2 applications.
4
4
 
5
- \* rclnodejs development and maintenance is limited to all active ROS 2 LTS releases and the Rolling development branch
5
+ Supported ROS 2 distributions include Humble, Jazzy, Kilted, and Rolling.
6
6
 
7
- Here's an example for how to create a ROS 2 node that publishes a string message in a few lines of JavaScript.
8
-
9
- ```JavaScript
7
+ ```javascript
10
8
  const rclnodejs = require('rclnodejs');
11
9
  rclnodejs.init().then(() => {
12
- const node = rclnodejs.createNode('publisher_example_node');
10
+ const node = new rclnodejs.Node('publisher_example_node');
13
11
  const publisher = node.createPublisher('std_msgs/msg/String', 'topic');
14
12
  publisher.publish(`Hello ROS 2 from rclnodejs`);
15
- rclnodejs.spin(node);
13
+ node.spin();
16
14
  });
17
15
  ```
18
16
 
17
+ This example assumes your ROS 2 environment is already sourced.
18
+
19
19
  ## Installation
20
20
 
21
21
  ### Prerequisites
22
22
 
23
23
  - [Node.js](https://nodejs.org/en/) version >= 16.13.0
24
- - [ROS 2 SDK](https://docs.ros.org/en/jazzy/Installation.html) - **Don't forget to [source the setup file](https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Configuring-ROS2-Environment.html#source-the-setup-files)**
24
+ - [ROS 2 SDK](https://docs.ros.org/en/jazzy/Installation.html)
25
+
26
+ Before installing or running rclnodejs, source your ROS 2 environment:
27
+
28
+ ```bash
29
+ source /opt/ros/<distro>/setup.bash
30
+ ```
25
31
 
26
32
  ### Install rclnodejs
27
33
 
@@ -29,11 +35,17 @@ rclnodejs.init().then(() => {
29
35
  npm i rclnodejs
30
36
  ```
31
37
 
32
- - **Note:** to install rclnodejs from GitHub: add `"rclnodejs":"RobotWebTools/rclnodejs#<branch>"` to your `package.json` dependency section.
38
+ To install from GitHub instead of npm, run:
39
+
40
+ ```bash
41
+ npm install RobotWebTools/rclnodejs#<branch>
42
+ ```
43
+
44
+ Or add `"rclnodejs":"RobotWebTools/rclnodejs#<branch>"` to your `package.json` dependencies.
33
45
 
34
46
  ### Prebuilt Binaries
35
47
 
36
- rclnodejs ships with prebuilt native binaries for common Linux configurations since `v1.5.2`, eliminating the need for compilation during installation. This significantly speeds up installation and reduces dependencies.
48
+ rclnodejs ships with prebuilt native binaries for common Linux configurations since `v1.5.2`, eliminating the need for compilation during installation. This applies to supported Linux environments when installing the published npm package.
37
49
 
38
50
  **Supported Platforms:**
39
51
 
@@ -42,26 +54,39 @@ rclnodejs ships with prebuilt native binaries for common Linux configurations si
42
54
  - **Architectures:** x64, arm64
43
55
  - **Node.js:** >= 16.20.2 (N-API compatible)
44
56
 
45
- **Force Building from Source:**
57
+ Installations outside this prebuilt matrix automatically fall back to building from source.
46
58
 
47
- If you need to build from source even when a prebuilt binary is available, set the environment variable:
59
+ **Force Building from Source:**
48
60
 
49
61
  ```bash
50
62
  export RCLNODEJS_FORCE_BUILD=1
51
63
  npm install rclnodejs
52
64
  ```
53
65
 
54
- ## Documentation
66
+ ## Documentation and Examples
55
67
 
56
- API [documentation](https://robotwebtools.github.io/rclnodejs/docs/index.html) is available online.
68
+ - API documentation: [robotwebtools.github.io/rclnodejs/docs](https://robotwebtools.github.io/rclnodejs/docs/index.html)
69
+ - Tutorials: [tutorials/](https://github.com/RobotWebTools/rclnodejs/tree/develop/tutorials)
70
+ - JavaScript examples: [example/](https://github.com/RobotWebTools/rclnodejs/tree/develop/example)
71
+ - TypeScript demos: [ts_demo/](https://github.com/RobotWebTools/rclnodejs/tree/develop/ts_demo)
72
+ - Electron demos: [electron_demo/](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo)
73
+ - Companion CLI: [rclnodejs-cli](https://github.com/RobotWebTools/rclnodejs-cli/)
57
74
 
58
- ## JavaScript Examples
75
+ ## Message Generation
59
76
 
60
- Try the [examples](https://github.com/RobotWebTools/rclnodejs/tree/develop/example) to get started.
77
+ rclnodejs generates JavaScript message interfaces and TypeScript declarations during installation for `rclnodejs > 1.5.0`. If you install additional ROS packages later, rerun the generator in your project:
78
+
79
+ ```bash
80
+ npx generate-ros-messages
81
+ ```
82
+
83
+ Generated files are written to `<your-project>/node_modules/rclnodejs/generated/`.
84
+
85
+ This step is only needed after adding ROS packages that were not present when rclnodejs was installed.
61
86
 
62
87
  ## Using rclnodejs with TypeScript
63
88
 
64
- TypeScript declaration files are included in the `types/` folder. Configure your `tsconfig.json`:
89
+ TypeScript declaration files are included in the package. In most projects, configuring your `tsconfig.json` is sufficient:
65
90
 
66
91
  ```jsonc
67
92
  {
@@ -73,33 +98,17 @@ TypeScript declaration files are included in the `types/` folder. Configure your
73
98
  }
74
99
  ```
75
100
 
76
- TypeScript example:
77
-
78
101
  ```typescript
79
102
  import * as rclnodejs from 'rclnodejs';
103
+
80
104
  rclnodejs.init().then(() => {
81
- const node = rclnodejs.createNode('publisher_example_node');
105
+ const node = new rclnodejs.Node('publisher_example_node');
82
106
  const publisher = node.createPublisher('std_msgs/msg/String', 'topic');
83
107
  publisher.publish(`Hello ROS 2 from rclnodejs`);
84
- rclnodejs.spin(node);
108
+ node.spin();
85
109
  });
86
110
  ```
87
111
 
88
- See [TypeScript demos](https://github.com/RobotWebTools/rclnodejs/tree/develop/ts_demo) for more examples.
89
-
90
- **Note** that the interface.d.ts file is updated each time the generate_messages.js script is run.
91
-
92
- ## Electron-based Visualization
93
-
94
- Create rich, interactive desktop applications using Electron and web technologies like Three.js. Build 3D visualizations, monitoring dashboards, and control interfaces that run on Windows, macOS, and Linux.
95
-
96
- | Demo | Description | Screenshot |
97
- | :-----------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------: |
98
- | **🐢 [turtle_tf2](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo/turtle_tf2)** | Real-time coordinate frame visualization with turtle control. Features TF2 transforms, keyboard control, and dynamic frame updates. | ![turtle_tf2](https://github.com/RobotWebTools/rclnodejs/blob/develop/electron_demo/turtle_tf2/turtle-tf2-demo.png?raw=true) |
99
- | **🦾 [manipulator](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo/manipulator)** | Interactive two-joint robotic arm simulation. Features 3D joint visualization, manual/automatic control, and visual movement markers. | ![manipulator](https://github.com/RobotWebTools/rclnodejs/blob/develop/electron_demo/manipulator/manipulator-demo.png?raw=true) |
100
-
101
- Explore more examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
102
-
103
112
  ## License
104
113
 
105
114
  Apache License Version 2.0
package/index.js CHANGED
@@ -39,6 +39,10 @@ const {
39
39
  } = require('./lib/parameter.js');
40
40
  const path = require('path');
41
41
  const QoS = require('./lib/qos.js');
42
+ const {
43
+ QoSPolicyKind,
44
+ QoSOverridingOptions,
45
+ } = require('./lib/qos_overriding_options.js');
42
46
  const rclnodejs = require('./lib/native_loader.js');
43
47
  const tsdGenerator = require('./rostsd_gen/index.js');
44
48
  const validator = require('./lib/validator.js');
@@ -63,7 +67,10 @@ const {
63
67
  const ParameterClient = require('./lib/parameter_client.js');
64
68
  const errors = require('./lib/errors.js');
65
69
  const ParameterWatcher = require('./lib/parameter_watcher.js');
70
+ const ParameterEventHandler = require('./lib/parameter_event_handler.js');
71
+ const waitForMessage = require('./lib/wait_for_message.js');
66
72
  const MessageIntrospector = require('./lib/message_introspector.js');
73
+ const MessageInfo = require('./lib/message_info.js');
67
74
  const ObservableSubscription = require('./lib/observable_subscription.js');
68
75
  const { spawn } = require('child_process');
69
76
  const {
@@ -243,12 +250,24 @@ let rcl = {
243
250
  /** {@link ParameterWatcher} class */
244
251
  ParameterWatcher: ParameterWatcher,
245
252
 
253
+ /** {@link ParameterEventHandler} class */
254
+ ParameterEventHandler: ParameterEventHandler,
255
+
256
+ /** {@link MessageInfo} class */
257
+ MessageInfo: MessageInfo,
258
+
246
259
  /** {@link ObservableSubscription} class */
247
260
  ObservableSubscription: ObservableSubscription,
248
261
 
249
262
  /** {@link QoS} class */
250
263
  QoS: QoS,
251
264
 
265
+ /** {@link QoSPolicyKind} enum */
266
+ QoSPolicyKind: QoSPolicyKind,
267
+
268
+ /** {@link QoSOverridingOptions} class */
269
+ QoSOverridingOptions: QoSOverridingOptions,
270
+
252
271
  /** {@link RMWUtils} */
253
272
  RMWUtils: RMWUtils,
254
273
 
@@ -458,6 +477,26 @@ let rcl = {
458
477
  node.spinOnce(timeout);
459
478
  },
460
479
 
480
+ /**
481
+ * Wait for a single message on a topic.
482
+ *
483
+ * Creates a temporary subscription, waits for the first message to arrive,
484
+ * and returns it. The temporary subscription is always cleaned up, even on
485
+ * timeout or error. The node must be spinning before calling this function.
486
+ *
487
+ * This is the rclnodejs equivalent of rclpy's `wait_for_message`.
488
+ *
489
+ * @param {function|string|object} typeClass - The ROS message type class.
490
+ * @param {Node} node - The node to create the temporary subscription on.
491
+ * @param {string} topic - The topic name to listen on.
492
+ * @param {object} [options] - Options.
493
+ * @param {number} [options.timeout] - Timeout in milliseconds. If omitted, waits indefinitely.
494
+ * @param {object} [options.qos] - QoS profile for the subscription.
495
+ * @returns {Promise<object>} - Resolves with the received message.
496
+ * @throws {Error} If timeout expires before a message arrives.
497
+ */
498
+ waitForMessage: waitForMessage,
499
+
461
500
  /**
462
501
  * Shutdown an RCL environment identified by a context. The shutdown process will
463
502
  * destroy all nodes and related resources in the context. If no context is
@@ -51,6 +51,12 @@ class ActionClient extends Entity {
51
51
  * @param {QoS} options.qos.feedbackSubQosProfile - Quality of service option for the feedback subscription,
52
52
  * default: new QoS(QoS.HistoryPolicy.RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT, 10).
53
53
  * @param {QoS} options.qos.statusSubQosProfile - Quality of service option for the status subscription, default: QoS.profileActionStatusDefault.
54
+ * @param {boolean} options.enableFeedbackMsgOptimization - Enable feedback subscription content filter to
55
+ * optimize the handling of feedback messages. When enabled, the content filter is used to configure
56
+ * the goal ID for the subscription, which helps avoid the reception of irrelevant feedback messages.
57
+ * An action client can handle up to 6 goals simultaneously with this optimization. If the number
58
+ * of goals exceeds the limit or the RMW doesn't support content filter, optimization is automatically
59
+ * disabled. Default: false.
54
60
  */
55
61
  constructor(node, typeClass, actionName, options) {
56
62
  super(null, null, options);
@@ -87,6 +93,15 @@ class ActionClient extends Entity {
87
93
  checkTypes: true,
88
94
  };
89
95
 
96
+ // Enable feedback subscription content filter optimization.
97
+ // Only supported on ROS2 Rolling and only effective when the native
98
+ // binding provides the required functions AND the RMW implementation
99
+ // actually supports content filtering on the feedback subscription.
100
+ this._enableFeedbackMsgOptimization =
101
+ this._options.enableFeedbackMsgOptimization === true &&
102
+ DistroUtils.getDistroId() >= DistroUtils.DistroId.ROLLING &&
103
+ typeof rclnodejs.actionConfigureFeedbackSubFilterAddGoalId === 'function';
104
+
90
105
  let type = this.typeClass.type();
91
106
 
92
107
  this._handle = rclnodejs.actionCreateClient(
@@ -126,6 +141,7 @@ class ActionClient extends Entity {
126
141
  }
127
142
 
128
143
  this._goalHandles.set(uuid, goalHandle);
144
+ this._feedbackSubFilterAddGoalId(goalHandle.goalId);
129
145
  } else {
130
146
  // Clean up feedback callback for rejected goals
131
147
  let uuid = ActionUuid.fromMessage(
@@ -205,6 +221,9 @@ class ActionClient extends Entity {
205
221
  status === ActionInterfaces.GoalStatus.STATUS_ABORTED
206
222
  ) {
207
223
  this._goalHandles.delete(uuid);
224
+ this._feedbackSubFilterRemoveGoalId(
225
+ statusMessage.goal_info.goal_id
226
+ );
208
227
  }
209
228
  }
210
229
  } else {
@@ -393,6 +412,8 @@ class ActionClient extends Entity {
393
412
  this._removePendingCancelRequest(sequenceNumber)
394
413
  );
395
414
 
415
+ this._feedbackSubFilterRemoveGoalId(goalHandle.goalId);
416
+
396
417
  return deferred.promise;
397
418
  }
398
419
 
@@ -442,9 +463,10 @@ class ActionClient extends Entity {
442
463
  goalHandle.status = result.status;
443
464
  return result.result;
444
465
  });
445
- deferred.setDoneCallback(() =>
446
- this._removePendingResultRequest(sequenceNumber)
447
- );
466
+ deferred.setDoneCallback(() => {
467
+ this._removePendingResultRequest(sequenceNumber);
468
+ this._feedbackSubFilterRemoveGoalId(goalHandle.goalId);
469
+ });
448
470
 
449
471
  this._pendingResultRequests.set(sequenceNumber, deferred);
450
472
 
@@ -464,6 +486,42 @@ class ActionClient extends Entity {
464
486
  this._pendingCancelRequests.delete(sequenceNumber);
465
487
  }
466
488
 
489
+ /**
490
+ * Add a goal ID to the feedback subscription content filter.
491
+ * @ignore
492
+ * @param {object} goalId - The goal UUID message.
493
+ */
494
+ _feedbackSubFilterAddGoalId(goalId) {
495
+ if (!this._enableFeedbackMsgOptimization) return;
496
+ try {
497
+ rclnodejs.actionConfigureFeedbackSubFilterAddGoalId(
498
+ this.handle,
499
+ Buffer.from(goalId.uuid)
500
+ );
501
+ } catch (e) {
502
+ this._enableFeedbackMsgOptimization = false;
503
+ this._node.getLogger().warn(`${e.message}`);
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Remove a goal ID from the feedback subscription content filter.
509
+ * @ignore
510
+ * @param {object} goalId - The goal UUID message.
511
+ */
512
+ _feedbackSubFilterRemoveGoalId(goalId) {
513
+ if (!this._enableFeedbackMsgOptimization) return;
514
+ try {
515
+ rclnodejs.actionConfigureFeedbackSubFilterRemoveGoalId(
516
+ this.handle,
517
+ Buffer.from(goalId.uuid)
518
+ );
519
+ } catch (e) {
520
+ this._enableFeedbackMsgOptimization = false;
521
+ this._node.getLogger().warn(`${e.message}`);
522
+ }
523
+ }
524
+
467
525
  /**
468
526
  * Destroy the underlying action client handle.
469
527
  * @return {undefined}
@@ -82,11 +82,26 @@ class ServerGoalHandle {
82
82
  }
83
83
 
84
84
  /**
85
- * Updates the goal handle with the execute status and begins exection.
85
+ * Transitions the goal to the executing state and begins execution.
86
+ * Has no effect if the goal is already executing, no longer active, or destroyed.
86
87
  * @param {function} callback - An optional callback to use instead of the one provided to the action server.
87
88
  * @returns {undefined}
88
89
  */
89
90
  execute(callback) {
91
+ if (this._destroyed) {
92
+ return;
93
+ }
94
+
95
+ // Guard: already executing — no-op
96
+ if (this.status === ActionInterfaces.GoalStatus.STATUS_EXECUTING) {
97
+ return;
98
+ }
99
+
100
+ // Guard: only transition if goal is still active
101
+ if (!this.isActive) {
102
+ return;
103
+ }
104
+
90
105
  if (!this.isCancelRequested) {
91
106
  this._updateState(GoalEvent.EXECUTE);
92
107
  }
@@ -105,6 +120,16 @@ class ServerGoalHandle {
105
120
  return;
106
121
  }
107
122
 
123
+ if (this.status !== ActionInterfaces.GoalStatus.STATUS_EXECUTING) {
124
+ this._actionServer._node
125
+ .getLogger()
126
+ .warn(
127
+ `publishFeedback() called on goal ${this.goalId.uuid} ` +
128
+ `in status ${this.status}, not executing; ignoring`
129
+ );
130
+ return;
131
+ }
132
+
108
133
  let feedbackMessage =
109
134
  new this._actionServer.typeClass.impl.FeedbackMessage();
110
135
  feedbackMessage['goal_id'] = this.goalId;
@@ -0,0 +1,94 @@
1
+ // Copyright (c) 2026, The Robot Web Tools Contributors
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
+ /**
18
+ * @class MessageInfo
19
+ *
20
+ * Contains metadata about a received message, including timestamps,
21
+ * sequence numbers, and the publisher's globally unique identifier (GID).
22
+ *
23
+ * This is the rclnodejs equivalent of rclpy's MessageInfo.
24
+ * It is passed as the second argument to subscription callbacks when the
25
+ * callback accepts two parameters.
26
+ *
27
+ * @example
28
+ * node.createSubscription(
29
+ * 'std_msgs/msg/String',
30
+ * 'topic',
31
+ * (msg, messageInfo) => {
32
+ * console.log('Source timestamp:', messageInfo.sourceTimestamp);
33
+ * console.log('Received at:', messageInfo.receivedTimestamp);
34
+ * console.log('Publisher GID:', messageInfo.publisherGid);
35
+ * }
36
+ * );
37
+ */
38
+ class MessageInfo {
39
+ /**
40
+ * Create a MessageInfo from a raw info object returned by the native layer.
41
+ *
42
+ * @param {object} rawInfo - Raw message info from rclTakeWithInfo
43
+ * @hideconstructor
44
+ */
45
+ constructor(rawInfo) {
46
+ /**
47
+ * The timestamp when the message was published (nanoseconds since epoch).
48
+ * @type {bigint}
49
+ */
50
+ this.sourceTimestamp = rawInfo.source_timestamp;
51
+
52
+ /**
53
+ * The timestamp when the message was received by the subscription (nanoseconds since epoch).
54
+ * @type {bigint}
55
+ */
56
+ this.receivedTimestamp = rawInfo.received_timestamp;
57
+
58
+ /**
59
+ * The publication sequence number assigned by the publisher.
60
+ * @type {bigint}
61
+ */
62
+ this.publicationSequenceNumber = rawInfo.publication_sequence_number;
63
+
64
+ /**
65
+ * The reception sequence number assigned by the subscriber.
66
+ * @type {bigint}
67
+ */
68
+ this.receptionSequenceNumber = rawInfo.reception_sequence_number;
69
+
70
+ /**
71
+ * The globally unique identifier (GID) of the publisher.
72
+ * A Buffer containing the raw GID bytes.
73
+ * @type {Buffer}
74
+ */
75
+ this.publisherGid = rawInfo.publisher_gid;
76
+ }
77
+
78
+ /**
79
+ * Convert to a plain object representation.
80
+ *
81
+ * @returns {object} Plain object with all metadata fields
82
+ */
83
+ toPlainObject() {
84
+ return {
85
+ sourceTimestamp: this.sourceTimestamp,
86
+ receivedTimestamp: this.receivedTimestamp,
87
+ publicationSequenceNumber: this.publicationSequenceNumber,
88
+ receptionSequenceNumber: this.receptionSequenceNumber,
89
+ publisherGid: this.publisherGid,
90
+ };
91
+ }
92
+ }
93
+
94
+ module.exports = MessageInfo;