diff --git a/index.js b/index.js index 22063127..504981dd 100644 --- a/index.js +++ b/index.js @@ -57,6 +57,7 @@ const { serializeMessage, deserializeMessage, } = require('./lib/serialization.js'); +const { spawn } = require('child_process'); /** * Get the version of the generator that was used for the currently present interfaces. @@ -89,6 +90,82 @@ async function getCurrentGeneratorVersion() { let _rosVersionChecked = false; +/** + * Run a ROS2 package executable using 'ros2 run' command. + * @param {string} packageName - The name of the ROS2 package. + * @param {string} executableName - The name of the executable to run. + * @param {string[]} [args=[]] - Additional arguments to pass to the executable. + * @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process. + */ +function ros2Run(packageName, executableName, args = []) { + return new Promise((resolve, reject) => { + if (typeof packageName !== 'string' || !packageName.trim()) { + reject(new Error('Package name must be a non-empty string')); + return; + } + + if (typeof executableName !== 'string' || !executableName.trim()) { + reject(new Error('Executable name must be a non-empty string')); + return; + } + + if (!Array.isArray(args)) { + reject(new Error('Arguments must be an array')); + return; + } + + const command = 'ros2'; + const cmdArgs = ['run', packageName, executableName, ...args]; + const childProcess = spawn(command, cmdArgs); + + childProcess.on('error', (error) => { + reject(new Error(`Failed to start ros2 run: ${error.message}`)); + }); + childProcess.on('spawn', () => { + resolve({ + process: childProcess, + }); + }); + }); +} + +/** + * Run a ROS2 launch file using 'ros2 launch' command. + * @param {string} packageName - The name of the ROS2 package. + * @param {string} launchFile - The name of the launch file to run. + * @param {string[]} [args=[]] - Additional arguments to pass to the launch file. + * @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process. + */ +function ros2Launch(packageName, launchFile, args = []) { + return new Promise((resolve, reject) => { + if (typeof packageName !== 'string' || !packageName.trim()) { + reject(new Error('Package name must be a non-empty string')); + return; + } + if (typeof launchFile !== 'string' || !launchFile.trim()) { + reject(new Error('Launch file name must be a non-empty string')); + return; + } + if (!Array.isArray(args)) { + reject(new Error('Arguments must be an array')); + return; + } + const command = 'ros2'; + const cmdArgs = ['launch', packageName, launchFile, ...args]; + const childProcess = spawn(command, cmdArgs); + + childProcess.on('error', (error) => { + reject(new Error(`Failed to start ros2 launch: ${error.message}`)); + }); + + childProcess.on('spawn', () => { + resolve({ + process: childProcess, + }); + }); + }); +} + /** * A module that exposes the rclnodejs interfaces. * @exports rclnodejs @@ -444,6 +521,24 @@ let rcl = { // this will not throw even if the handler is already removed process.removeListener('SIGINT', _sigHandler); }, + + /** + * Run a ROS2 package executable using 'ros2 run' command. + * @param {string} packageName - The name of the ROS2 package. + * @param {string} executableName - The name of the executable to run. + * @param {string[]} [args=[]] - Additional arguments to pass to the executable. + * @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process. + */ + ros2Run: ros2Run, + + /** + * Run a ROS2 launch file using 'ros2 launch' command. + * @param {string} packageName - The name of the ROS2 package. + * @param {string} launchFile - The name of the launch file to run. + * @param {string[]} [args=[]] - Additional arguments to pass to the launch file. + * @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process. + */ + ros2Launch: ros2Launch, }; const _sigHandler = () => { diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index d5b767ce..57d26924 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -2,6 +2,7 @@ import { expectType, expectAssignable } from 'tsd'; import * as rclnodejs from 'rclnodejs'; +import { ChildProcess } from 'child_process'; const NODE_NAME = 'test_node'; const LIFECYCLE_NODE_NAME = 'lifecycle_test_node'; @@ -17,6 +18,12 @@ expectType(rclnodejs.DistroUtils.getDistroName()); expectType(rclnodejs.isShutdown()); expectType(rclnodejs.shutdown()); expectType(rclnodejs.removeSignalHandlers()); +expectType>( + rclnodejs.ros2Run('package_name', 'executable_name', ['arg1', 'arg2']) +); +expectType>( + rclnodejs.ros2Launch('package_name', 'launch_file', ['arg1', 'arg2']) +); // ---- DistroUtil ---- expectType(rclnodejs.DistroUtils.getDistroId()); diff --git a/types/index.d.ts b/types/index.d.ts index 94b7b19a..b70b1f6e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,7 @@ /// +import { ChildProcess } from 'child_process'; + declare module 'rclnodejs' { type Class = new (...args: any[]) => any; @@ -207,4 +209,30 @@ declare module 'rclnodejs' { * @returns An Object representing the deserialized message. */ function deserializeMessage(buffer: Buffer, typeClass: Class): object; + + /** + * Run a ROS2 package executable using 'ros2 run' command. + * @param {string} packageName - The name of the ROS2 package. + * @param {string} executableName - The name of the executable to run. + * @param {string[]} [args=[]] - Additional arguments to pass to the executable. + * @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process. + */ + function ros2Run( + packageName: string, + executableName: string, + args: string[] + ): Promise<{ process: ChildProcess }>; + + /** + * Run a ROS2 launch file using 'ros2 launch' command. + * @param {string} packageName - The name of the ROS2 package. + * @param {string} launchFile - The name of the launch file to run. + * @param {string[]} [args=[]] - Additional arguments to pass to the launch file. + * @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process. + */ + function ros2Launch( + packageName: string, + launchFile: string, + args: string[] + ): Promise<{ process: ChildProcess }>; }