rclnodejs 1.4.2 → 1.5.1
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/README.md +20 -115
- package/index.js +95 -0
- package/lib/client.js +8 -0
- package/lib/interface_loader.js +97 -1
- package/lib/publisher.js +9 -0
- package/lib/service.js +8 -0
- package/lib/subscription.js +9 -0
- package/package.json +1 -1
- package/rosidl_gen/generate_worker.js +63 -0
- package/rosidl_gen/index.js +31 -0
- package/types/client.d.ts +5 -0
- package/types/index.d.ts +28 -0
- package/types/publisher.d.ts +5 -0
- package/types/service.d.ts +5 -0
- package/types/subscription.d.ts +5 -0
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
# rclnodejs [](https://github.com/RobotWebTools/rclnodejs/actions/workflows/linux-x64-push-test.yml?query=branch%3Adevelop)[](https://github.com/RobotWebTools/rclnodejs/actions/workflows/linux-arm64-push-test.yml?query=branch%3Adevelop)
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
+
\* rclnodejs development and maintenance is limited to all active ROS 2 LTS releases and the Rolling development branch
|
|
6
|
+
|
|
5
7
|
Here's an example for how to create a ROS 2 node that publishes a string message in a few lines of JavaScript.
|
|
6
8
|
|
|
7
9
|
```JavaScript
|
|
@@ -14,38 +16,20 @@ rclnodejs.init().then(() => {
|
|
|
14
16
|
});
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
**Node.js**
|
|
20
|
-
|
|
21
|
-
- [Node.js](https://nodejs.org/en/) version >= 16.13.0.
|
|
19
|
+
## Installation
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
### Prerequisites
|
|
24
22
|
|
|
25
|
-
-
|
|
26
|
-
- **
|
|
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)**
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Install the rclnodejs version that is compatible with your installed version of ROS 2 (see table below).
|
|
31
|
-
|
|
32
|
-
Run the following command for the most current version of rclnodejs
|
|
26
|
+
### Install rclnodejs
|
|
33
27
|
|
|
34
28
|
```bash
|
|
35
29
|
npm i rclnodejs
|
|
36
30
|
```
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm i rclnodejs@x.y.z
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
#### RCLNODEJS - ROS 2 Version Compatibility
|
|
45
|
-
|
|
46
|
-
| RCLNODEJS Version | Compatible ROS 2 LTS |
|
|
47
|
-
| :----------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
|
48
|
-
| latest version (currently [v1.4.2](https://github.com/RobotWebTools/rclnodejs/tree/1.4.2)) | [Kilted](https://github.com/RobotWebTools/rclnodejs/tree/kilted)<br>[Jazzy](https://github.com/RobotWebTools/rclnodejs/tree/jazzy)<br>[Humble](https://github.com/RobotWebTools/rclnodejs/tree/humble-hawksbill) |
|
|
32
|
+
- **Note:** to install rclnodejs from GitHub: add `"rclnodejs":"RobotWebTools/rclnodejs#<branch>"` to your `package.json` dependency section.
|
|
49
33
|
|
|
50
34
|
## Documentation
|
|
51
35
|
|
|
@@ -53,98 +37,23 @@ API [documentation](https://robotwebtools.github.io/rclnodejs/docs/index.html) i
|
|
|
53
37
|
|
|
54
38
|
## JavaScript Examples
|
|
55
39
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Use complex message
|
|
59
|
-
|
|
60
|
-
```JavaScript
|
|
61
|
-
const publisher = node.createPublisher('sensor_msgs/msg/JointState', 'topic');
|
|
62
|
-
publisher.publish({
|
|
63
|
-
header: {
|
|
64
|
-
stamp: {
|
|
65
|
-
sec: 123456,
|
|
66
|
-
nanosec: 789,
|
|
67
|
-
},
|
|
68
|
-
frame_id: 'main frame',
|
|
69
|
-
},
|
|
70
|
-
name: ['Tom', 'Jerry'],
|
|
71
|
-
position: [1, 2],
|
|
72
|
-
velocity: [2, 3],
|
|
73
|
-
effort: [4, 5, 6],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Create a subscription
|
|
79
|
-
|
|
80
|
-
```JavaScript
|
|
81
|
-
const rclnodejs = require('../index.js');
|
|
82
|
-
|
|
83
|
-
// Create a ROS node and then print out the string message received from publishers
|
|
84
|
-
rclnodejs.init().then(() => {
|
|
85
|
-
const node = rclnodejs.createNode('subscription_example_node');
|
|
86
|
-
|
|
87
|
-
node.createSubscription('std_msgs/msg/String', 'topic', (msg) => {
|
|
88
|
-
console.log(`Received message: ${typeof msg}`, msg);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
rclnodejs.spin(node);
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Create a service
|
|
96
|
-
|
|
97
|
-
```JavaScript
|
|
98
|
-
node.createService('example_interfaces/srv/AddTwoInts', 'add_two_ints', (request, response) => {
|
|
99
|
-
console.log(`Incoming request: ${typeof request}`, request);
|
|
100
|
-
let result = response.template;
|
|
101
|
-
result.sum = request.a + request.b;
|
|
102
|
-
console.log(`Sending response: ${typeof result}`, result, '\n--');
|
|
103
|
-
response.send(result);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Send a request in a client
|
|
109
|
-
|
|
110
|
-
```JavaScript
|
|
111
|
-
const client = node.createClient('example_interfaces/srv/AddTwoInts', 'add_two_ints');
|
|
112
|
-
const request = {
|
|
113
|
-
a: Math.floor(Math.random() * 100),
|
|
114
|
-
b: Math.floor(Math.random() * 100),
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
console.log(`Sending: ${typeof request}`, request);
|
|
118
|
-
client.sendRequest(request, (response) => {
|
|
119
|
-
console.log(`Result: ${typeof response}`, response);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Check out more [examples](https://github.com/RobotWebTools/rclnodejs/tree/develop/example).
|
|
40
|
+
Try the [examples](https://github.com/RobotWebTools/rclnodejs/tree/develop/example) to get started.
|
|
125
41
|
|
|
126
42
|
## Using rclnodejs with TypeScript
|
|
127
43
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
npm install typescript @types/node -D
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
In your project's tsconfig.json file include the following compiler options:
|
|
44
|
+
TypeScript declaration files are included in the `types/` folder. Configure your `tsconfig.json`:
|
|
135
45
|
|
|
136
46
|
```jsonc
|
|
137
47
|
{
|
|
138
48
|
"compilerOptions": {
|
|
139
49
|
"module": "commonjs",
|
|
140
50
|
"moduleResolution": "node",
|
|
141
|
-
"target": "
|
|
142
|
-
|
|
143
|
-
}
|
|
51
|
+
"target": "es2020",
|
|
52
|
+
},
|
|
144
53
|
}
|
|
145
54
|
```
|
|
146
55
|
|
|
147
|
-
|
|
56
|
+
TypeScript example:
|
|
148
57
|
|
|
149
58
|
```typescript
|
|
150
59
|
import * as rclnodejs from 'rclnodejs';
|
|
@@ -156,21 +65,17 @@ rclnodejs.init().then(() => {
|
|
|
156
65
|
});
|
|
157
66
|
```
|
|
158
67
|
|
|
159
|
-
|
|
68
|
+
See [TypeScript demos](https://github.com/RobotWebTools/rclnodejs/tree/develop/ts_demo) for more examples.
|
|
160
69
|
|
|
161
|
-
|
|
162
|
-
const msg: rclnodejs.std_msgs.msg.String = {
|
|
163
|
-
data: 'hello ROS2 from rclnodejs',
|
|
164
|
-
};
|
|
165
|
-
```
|
|
70
|
+
**Note** that the interface.d.ts file is updated each time the generate_messages.js script is run.
|
|
166
71
|
|
|
167
|
-
|
|
72
|
+
## Electron-based Visualization
|
|
168
73
|
|
|
169
|
-
|
|
74
|
+
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.
|
|
170
75
|
|
|
171
|
-
|
|
76
|
+
Try the `electron_demo/turtle_tf2` demo for real-time coordinate frame visualization with dynamic transforms and keyboard-controlled turtle movement. More examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
|
|
172
77
|
|
|
173
|
-
|
|
78
|
+

|
|
174
79
|
|
|
175
80
|
## License
|
|
176
81
|
|
package/index.js
CHANGED
|
@@ -57,6 +57,7 @@ const {
|
|
|
57
57
|
serializeMessage,
|
|
58
58
|
deserializeMessage,
|
|
59
59
|
} = require('./lib/serialization.js');
|
|
60
|
+
const { spawn } = require('child_process');
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* Get the version of the generator that was used for the currently present interfaces.
|
|
@@ -89,6 +90,82 @@ async function getCurrentGeneratorVersion() {
|
|
|
89
90
|
|
|
90
91
|
let _rosVersionChecked = false;
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Run a ROS2 package executable using 'ros2 run' command.
|
|
95
|
+
* @param {string} packageName - The name of the ROS2 package.
|
|
96
|
+
* @param {string} executableName - The name of the executable to run.
|
|
97
|
+
* @param {string[]} [args=[]] - Additional arguments to pass to the executable.
|
|
98
|
+
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
|
|
99
|
+
*/
|
|
100
|
+
function ros2Run(packageName, executableName, args = []) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
if (typeof packageName !== 'string' || !packageName.trim()) {
|
|
103
|
+
reject(new Error('Package name must be a non-empty string'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof executableName !== 'string' || !executableName.trim()) {
|
|
108
|
+
reject(new Error('Executable name must be a non-empty string'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!Array.isArray(args)) {
|
|
113
|
+
reject(new Error('Arguments must be an array'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const command = 'ros2';
|
|
118
|
+
const cmdArgs = ['run', packageName, executableName, ...args];
|
|
119
|
+
const childProcess = spawn(command, cmdArgs);
|
|
120
|
+
|
|
121
|
+
childProcess.on('error', (error) => {
|
|
122
|
+
reject(new Error(`Failed to start ros2 run: ${error.message}`));
|
|
123
|
+
});
|
|
124
|
+
childProcess.on('spawn', () => {
|
|
125
|
+
resolve({
|
|
126
|
+
process: childProcess,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run a ROS2 launch file using 'ros2 launch' command.
|
|
134
|
+
* @param {string} packageName - The name of the ROS2 package.
|
|
135
|
+
* @param {string} launchFile - The name of the launch file to run.
|
|
136
|
+
* @param {string[]} [args=[]] - Additional arguments to pass to the launch file.
|
|
137
|
+
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
|
|
138
|
+
*/
|
|
139
|
+
function ros2Launch(packageName, launchFile, args = []) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
if (typeof packageName !== 'string' || !packageName.trim()) {
|
|
142
|
+
reject(new Error('Package name must be a non-empty string'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (typeof launchFile !== 'string' || !launchFile.trim()) {
|
|
146
|
+
reject(new Error('Launch file name must be a non-empty string'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!Array.isArray(args)) {
|
|
150
|
+
reject(new Error('Arguments must be an array'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const command = 'ros2';
|
|
154
|
+
const cmdArgs = ['launch', packageName, launchFile, ...args];
|
|
155
|
+
const childProcess = spawn(command, cmdArgs);
|
|
156
|
+
|
|
157
|
+
childProcess.on('error', (error) => {
|
|
158
|
+
reject(new Error(`Failed to start ros2 launch: ${error.message}`));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
childProcess.on('spawn', () => {
|
|
162
|
+
resolve({
|
|
163
|
+
process: childProcess,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
92
169
|
/**
|
|
93
170
|
* A module that exposes the rclnodejs interfaces.
|
|
94
171
|
* @exports rclnodejs
|
|
@@ -444,6 +521,24 @@ let rcl = {
|
|
|
444
521
|
// this will not throw even if the handler is already removed
|
|
445
522
|
process.removeListener('SIGINT', _sigHandler);
|
|
446
523
|
},
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Run a ROS2 package executable using 'ros2 run' command.
|
|
527
|
+
* @param {string} packageName - The name of the ROS2 package.
|
|
528
|
+
* @param {string} executableName - The name of the executable to run.
|
|
529
|
+
* @param {string[]} [args=[]] - Additional arguments to pass to the executable.
|
|
530
|
+
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
|
|
531
|
+
*/
|
|
532
|
+
ros2Run: ros2Run,
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Run a ROS2 launch file using 'ros2 launch' command.
|
|
536
|
+
* @param {string} packageName - The name of the ROS2 package.
|
|
537
|
+
* @param {string} launchFile - The name of the launch file to run.
|
|
538
|
+
* @param {string[]} [args=[]] - Additional arguments to pass to the launch file.
|
|
539
|
+
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
|
|
540
|
+
*/
|
|
541
|
+
ros2Launch: ros2Launch,
|
|
447
542
|
};
|
|
448
543
|
|
|
449
544
|
const _sigHandler = () => {
|
package/lib/client.js
CHANGED
|
@@ -157,6 +157,14 @@ class Client extends Entity {
|
|
|
157
157
|
introspectionState
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get the logger name for this client.
|
|
163
|
+
* @returns {string} The logger name for this client.
|
|
164
|
+
*/
|
|
165
|
+
get loggerName() {
|
|
166
|
+
return rclnodejs.getNodeLoggerName(this._nodeHandle);
|
|
167
|
+
}
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
module.exports = Client;
|
package/lib/interface_loader.js
CHANGED
|
@@ -90,6 +90,95 @@ let interfaceLoader = {
|
|
|
90
90
|
return pkg;
|
|
91
91
|
},
|
|
92
92
|
|
|
93
|
+
_isRos2InstallationPath(pkgPath) {
|
|
94
|
+
// Use "which ros2" to dynamically find the ROS2 installation root
|
|
95
|
+
try {
|
|
96
|
+
const whichResult = require('child_process').spawnSync(
|
|
97
|
+
'which',
|
|
98
|
+
['ros2'],
|
|
99
|
+
{
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
timeout: 5000,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (whichResult.status === 0 && whichResult.stdout) {
|
|
106
|
+
const ros2BinPath = whichResult.stdout.trim();
|
|
107
|
+
// Get the ROS2 installation root (typically /opt/ros/<distro> or similar)
|
|
108
|
+
const ros2Root = path.dirname(path.dirname(ros2BinPath));
|
|
109
|
+
|
|
110
|
+
return pkgPath.includes(ros2Root);
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('Error running which ros2:', err.message);
|
|
114
|
+
// If "which ros2" fails, fall back to hardcoded check
|
|
115
|
+
return pkgPath.includes('ros2-linux');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return false;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
_searchAndGenerateInterface(packageName, type, messageName, filePath) {
|
|
122
|
+
// Check if it's a valid package
|
|
123
|
+
for (const pkgPath of generator.getInstalledPackagePaths()) {
|
|
124
|
+
// We are going to ignore the path where ROS2 is installed.
|
|
125
|
+
if (this._isRos2InstallationPath(pkgPath)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Recursively search for files named messageName.* under pkgPath/
|
|
130
|
+
if (fs.existsSync(pkgPath)) {
|
|
131
|
+
// Recursive function to search for files
|
|
132
|
+
function searchForFile(dir) {
|
|
133
|
+
try {
|
|
134
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
135
|
+
for (const item of items) {
|
|
136
|
+
const fullPath = path.join(dir, item.name);
|
|
137
|
+
|
|
138
|
+
if (item.isFile()) {
|
|
139
|
+
const baseName = path.parse(item.name).name;
|
|
140
|
+
// Check if the base filename matches messageName
|
|
141
|
+
if (baseName === messageName) {
|
|
142
|
+
return fullPath;
|
|
143
|
+
}
|
|
144
|
+
} else if (item.isDirectory()) {
|
|
145
|
+
// Recursively search subdirectories
|
|
146
|
+
const result = searchForFile(fullPath);
|
|
147
|
+
if (result) {
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
// Skip directories we can't read
|
|
154
|
+
console.error('Error reading directory:', dir, err.message);
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const foundFilePath = searchForFile(
|
|
160
|
+
path.join(pkgPath, 'share', packageName)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (foundFilePath && foundFilePath.length > 0) {
|
|
164
|
+
// Use worker thread to generate interfaces synchronously
|
|
165
|
+
try {
|
|
166
|
+
generator.generateInPathSyncWorker(pkgPath);
|
|
167
|
+
// Now try to load the interface again from the generated files
|
|
168
|
+
if (fs.existsSync(filePath)) {
|
|
169
|
+
return require(filePath);
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('Error in interface generation:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw new Error(
|
|
178
|
+
`The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
|
|
93
182
|
loadInterface(packageName, type, messageName) {
|
|
94
183
|
if (arguments.length === 1) {
|
|
95
184
|
const type = arguments[0];
|
|
@@ -100,7 +189,6 @@ let interfaceLoader = {
|
|
|
100
189
|
}
|
|
101
190
|
throw new Error(`The message required does not exist: ${type}`);
|
|
102
191
|
}
|
|
103
|
-
|
|
104
192
|
if (packageName && type && messageName) {
|
|
105
193
|
let filePath = path.join(
|
|
106
194
|
generator.generatedRoot,
|
|
@@ -110,8 +198,16 @@ let interfaceLoader = {
|
|
|
110
198
|
|
|
111
199
|
if (fs.existsSync(filePath)) {
|
|
112
200
|
return require(filePath);
|
|
201
|
+
} else {
|
|
202
|
+
return this._searchAndGenerateInterface(
|
|
203
|
+
packageName,
|
|
204
|
+
type,
|
|
205
|
+
messageName,
|
|
206
|
+
filePath
|
|
207
|
+
);
|
|
113
208
|
}
|
|
114
209
|
}
|
|
210
|
+
// We cannot parse `packageName`, `type` and `messageName` from the string passed.
|
|
115
211
|
throw new Error(
|
|
116
212
|
`The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`
|
|
117
213
|
);
|
package/lib/publisher.js
CHANGED
|
@@ -26,6 +26,7 @@ const Entity = require('./entity.js');
|
|
|
26
26
|
class Publisher extends Entity {
|
|
27
27
|
constructor(handle, typeClass, topic, options, node, eventCallbacks) {
|
|
28
28
|
super(handle, typeClass, options);
|
|
29
|
+
this._node = node;
|
|
29
30
|
if (node && eventCallbacks) {
|
|
30
31
|
this._events = eventCallbacks.createEventHandlers(this.handle);
|
|
31
32
|
node._events.push(...this._events);
|
|
@@ -126,6 +127,14 @@ class Publisher extends Entity {
|
|
|
126
127
|
set events(events) {
|
|
127
128
|
this._events = events;
|
|
128
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the logger name for this publisher.
|
|
133
|
+
* @returns {string} The logger name for this publisher.
|
|
134
|
+
*/
|
|
135
|
+
get loggerName() {
|
|
136
|
+
return rclnodejs.getNodeLoggerName(this._node.handle);
|
|
137
|
+
}
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
module.exports = Publisher;
|
package/lib/service.js
CHANGED
|
@@ -149,6 +149,14 @@ class Service extends Entity {
|
|
|
149
149
|
getOptions() {
|
|
150
150
|
return rclnodejs.getOptions(this._handle);
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the logger name for this service.
|
|
155
|
+
* @returns {string} The logger name for this service.
|
|
156
|
+
*/
|
|
157
|
+
get loggerName() {
|
|
158
|
+
return rclnodejs.getNodeLoggerName(this._nodeHandle);
|
|
159
|
+
}
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
module.exports = Service;
|
package/lib/subscription.js
CHANGED
|
@@ -42,6 +42,7 @@ class Subscription extends Entity {
|
|
|
42
42
|
this._topic = topic;
|
|
43
43
|
this._callback = callback;
|
|
44
44
|
this._isRaw = options.isRaw || false;
|
|
45
|
+
this._node = node;
|
|
45
46
|
|
|
46
47
|
if (node && eventCallbacks) {
|
|
47
48
|
this._events = eventCallbacks.createEventHandlers(this.handle);
|
|
@@ -168,6 +169,14 @@ class Subscription extends Entity {
|
|
|
168
169
|
set events(events) {
|
|
169
170
|
this._events = events;
|
|
170
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the logger name for this subscription.
|
|
175
|
+
* @returns {string} The logger name for this subscription.
|
|
176
|
+
*/
|
|
177
|
+
get loggerName() {
|
|
178
|
+
return rclnodejs.getNodeLoggerName(this._node.handle);
|
|
179
|
+
}
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
module.exports = Subscription;
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright (c) 2025, 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
|
+
const fse = require('fs-extra');
|
|
16
|
+
const generateJSStructFromIDL = require('./idl_generator.js');
|
|
17
|
+
const packages = require('./packages.js');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const idlConvertor = require('../rosidl_convertor/idl_convertor.js');
|
|
20
|
+
|
|
21
|
+
const generatedRoot = path.join(__dirname, '../generated/');
|
|
22
|
+
const idlPath = path.join(generatedRoot, 'share');
|
|
23
|
+
const useIDL = !!process.argv.find((arg) => arg === '--idl');
|
|
24
|
+
|
|
25
|
+
// Get target path from environment variable instead of workerData
|
|
26
|
+
const targetPath = process.env.WORKER_TARGET_PATH;
|
|
27
|
+
|
|
28
|
+
async function generateInPath(targetPath) {
|
|
29
|
+
let pkgsInfo = null;
|
|
30
|
+
if (!useIDL) {
|
|
31
|
+
pkgsInfo = Array.from(
|
|
32
|
+
(await packages.findPackagesInDirectory(targetPath)).values()
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
const idlPkgs = await packages.findPackagesInDirectory(targetPath, useIDL);
|
|
36
|
+
await fse.ensureDir(idlPath);
|
|
37
|
+
const promises = [];
|
|
38
|
+
idlPkgs.forEach((pkg) => {
|
|
39
|
+
pkg.idls.forEach((idl) => {
|
|
40
|
+
promises.push(idlConvertor(idl.pkgName, idl.filePath, idlPath));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
await Promise.all(promises);
|
|
44
|
+
const pkgsFromIdl = await packages.findPackagesInDirectory(idlPath, false);
|
|
45
|
+
pkgsInfo = Array.from(pkgsFromIdl.values());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await Promise.all(
|
|
49
|
+
pkgsInfo.map((pkgInfo) => generateJSStructFromIDL(pkgInfo, generatedRoot))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
try {
|
|
55
|
+
await generateInPath(targetPath);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Worker generation failed:', error.message);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main();
|
package/rosidl_gen/index.js
CHANGED
|
@@ -53,6 +53,35 @@ async function generateInPath(path) {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function generateInPathSyncWorker(targetPath) {
|
|
57
|
+
try {
|
|
58
|
+
// Use child_process.spawnSync for truly synchronous execution
|
|
59
|
+
const result = require('child_process').spawnSync(
|
|
60
|
+
'node',
|
|
61
|
+
[path.join(__dirname, 'generate_worker.js')],
|
|
62
|
+
{
|
|
63
|
+
env: { ...process.env, WORKER_TARGET_PATH: targetPath },
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
timeout: 30000,
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (result.error) {
|
|
70
|
+
throw result.error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (result.status !== 0) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Worker process exited with code ${result.status}. stderr: ${result.stderr}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result.stdout;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
async function generateAll(forcedGenerating) {
|
|
57
86
|
// If we want to create the JavaScript files compulsively (|forcedGenerating| equals to true)
|
|
58
87
|
// or the JavaScript files have not been created (|exist| equals to false),
|
|
@@ -86,7 +115,9 @@ const generator = {
|
|
|
86
115
|
|
|
87
116
|
generateAll,
|
|
88
117
|
generateInPath,
|
|
118
|
+
generateInPathSyncWorker,
|
|
89
119
|
generatedRoot,
|
|
120
|
+
getInstalledPackagePaths,
|
|
90
121
|
};
|
|
91
122
|
|
|
92
123
|
module.exports = generator;
|
package/types/client.d.ts
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference path="./base.d.ts" />
|
|
2
2
|
|
|
3
|
+
import { ChildProcess } from 'child_process';
|
|
4
|
+
|
|
3
5
|
declare module 'rclnodejs' {
|
|
4
6
|
type Class = new (...args: any[]) => any;
|
|
5
7
|
|
|
@@ -207,4 +209,30 @@ declare module 'rclnodejs' {
|
|
|
207
209
|
* @returns An Object representing the deserialized message.
|
|
208
210
|
*/
|
|
209
211
|
function deserializeMessage(buffer: Buffer, typeClass: Class): object;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Run a ROS2 package executable using 'ros2 run' command.
|
|
215
|
+
* @param {string} packageName - The name of the ROS2 package.
|
|
216
|
+
* @param {string} executableName - The name of the executable to run.
|
|
217
|
+
* @param {string[]} [args=[]] - Additional arguments to pass to the executable.
|
|
218
|
+
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
|
|
219
|
+
*/
|
|
220
|
+
function ros2Run(
|
|
221
|
+
packageName: string,
|
|
222
|
+
executableName: string,
|
|
223
|
+
args: string[]
|
|
224
|
+
): Promise<{ process: ChildProcess }>;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Run a ROS2 launch file using 'ros2 launch' command.
|
|
228
|
+
* @param {string} packageName - The name of the ROS2 package.
|
|
229
|
+
* @param {string} launchFile - The name of the launch file to run.
|
|
230
|
+
* @param {string[]} [args=[]] - Additional arguments to pass to the launch file.
|
|
231
|
+
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
|
|
232
|
+
*/
|
|
233
|
+
function ros2Launch(
|
|
234
|
+
packageName: string,
|
|
235
|
+
launchFile: string,
|
|
236
|
+
args: string[]
|
|
237
|
+
): Promise<{ process: ChildProcess }>;
|
|
210
238
|
}
|
package/types/publisher.d.ts
CHANGED
|
@@ -37,5 +37,10 @@ declare module 'rclnodejs' {
|
|
|
37
37
|
* @return {boolean} `true` if all published message data is acknowledged before the timeout, otherwise `false`.
|
|
38
38
|
*/
|
|
39
39
|
waitForAllAcked(timeout: bigint): boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the logger name for this publisher.
|
|
43
|
+
*/
|
|
44
|
+
readonly loggerName: string;
|
|
40
45
|
}
|
|
41
46
|
}
|
package/types/service.d.ts
CHANGED