Skip to content

Commit a4ad0ca

Browse files
committed
Add functions to run ros2 run/launch (#1222)
This PR adds functions to run ROS2 package executables and launch files programmatically from within Node.js applications using the `ros2 run` and `ros2 launch` commands. - Adds `ros2Run()` function to execute ROS2 package executables with optional arguments - Adds `ros2Launch()` function to run ROS2 launch files with optional arguments - Includes comprehensive input validation for both functions Fix: #1220
1 parent b444cf7 commit a4ad0ca

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

index.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const {
5757
serializeMessage,
5858
deserializeMessage,
5959
} = require('./lib/serialization.js');
60+
const { spawn } = require('child_process');
6061

6162
/**
6263
* Get the version of the generator that was used for the currently present interfaces.
@@ -89,6 +90,82 @@ async function getCurrentGeneratorVersion() {
8990

9091
let _rosVersionChecked = false;
9192

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+
92169
/**
93170
* A module that exposes the rclnodejs interfaces.
94171
* @exports rclnodejs
@@ -444,6 +521,24 @@ let rcl = {
444521
// this will not throw even if the handler is already removed
445522
process.removeListener('SIGINT', _sigHandler);
446523
},
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,
447542
};
448543

449544
const _sigHandler = () => {

test/types/index.test-d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { expectType, expectAssignable } from 'tsd';
44
import * as rclnodejs from 'rclnodejs';
5+
import { ChildProcess } from 'child_process';
56

67
const NODE_NAME = 'test_node';
78
const LIFECYCLE_NODE_NAME = 'lifecycle_test_node';
@@ -17,6 +18,12 @@ expectType<string | undefined>(rclnodejs.DistroUtils.getDistroName());
1718
expectType<boolean>(rclnodejs.isShutdown());
1819
expectType<void>(rclnodejs.shutdown());
1920
expectType<void>(rclnodejs.removeSignalHandlers());
21+
expectType<Promise<{ process: ChildProcess }>>(
22+
rclnodejs.ros2Run('package_name', 'executable_name', ['arg1', 'arg2'])
23+
);
24+
expectType<Promise<{ process: ChildProcess }>>(
25+
rclnodejs.ros2Launch('package_name', 'launch_file', ['arg1', 'arg2'])
26+
);
2027

2128
// ---- DistroUtil ----
2229
expectType<rclnodejs.DistroUtils.DistroId>(rclnodejs.DistroUtils.getDistroId());

types/index.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/// <reference path="./base.d.ts" />
22

3+
import { ChildProcess } from 'child_process';
4+
35
declare module 'rclnodejs' {
46
type Class = new (...args: any[]) => any;
57

@@ -207,4 +209,30 @@ declare module 'rclnodejs' {
207209
* @returns An Object representing the deserialized message.
208210
*/
209211
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 }>;
210238
}

0 commit comments

Comments
 (0)