UNPKG

rclnodejs

Version:
571 lines (491 loc) 19.9 kB
// Copyright (c) 2017 Intel Corporation. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict'; const DistroUtils = require('./lib/distro.js'); const RMWUtils = require('./lib/rmw.js'); const { Clock, ROSClock } = require('./lib/clock.js'); const ClockType = require('./lib/clock_type.js'); const compareVersions = require('compare-versions'); const Context = require('./lib/context.js'); const debug = require('debug')('rclnodejs'); const Duration = require('./lib/duration.js'); const fs = require('fs'); const generator = require('./rosidl_gen/index.js'); const loader = require('./lib/interface_loader.js'); const logging = require('./lib/logging.js'); const NodeOptions = require('./lib/node_options.js'); const { FloatingPointRange, IntegerRange, Parameter, ParameterDescriptor, ParameterType, DEFAULT_NUMERIC_RANGE_TOLERANCE, } = require('./lib/parameter.js'); const path = require('path'); const QoS = require('./lib/qos.js'); const rclnodejs = require('bindings')('rclnodejs'); const tsdGenerator = require('./rostsd_gen/index.js'); const validator = require('./lib/validator.js'); const Time = require('./lib/time.js'); const ActionClient = require('./lib/action/client.js'); const ActionServer = require('./lib/action/server.js'); const ActionUuid = require('./lib/action/uuid.js'); const ClientGoalHandle = require('./lib/action/client_goal_handle.js'); const { CancelResponse, GoalResponse } = require('./lib/action/response.js'); const ServerGoalHandle = require('./lib/action/server_goal_handle.js'); const { getActionClientNamesAndTypesByNode, getActionServerNamesAndTypesByNode, getActionNamesAndTypes, } = require('./lib/action/graph.js'); const ServiceIntrospectionStates = require('./lib/service_introspection.js'); 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. * @return {Promise<string | null>} The current version or null if the *generator.json* file was not found * @throws {Error} if there was an error reading the *generator.json* file (except for it being absent) */ async function getCurrentGeneratorVersion() { const jsonFilePath = path.join(generator.generatedRoot, 'generator.json'); return new Promise((resolve, reject) => { fs.open(jsonFilePath, 'r', (err) => { if (err) { if (err.code === 'ENOENT') { resolve(null); } else { reject(err); } } else { fs.readFile(jsonFilePath, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(JSON.parse(data).version); } }); } }); }); } 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 */ let rcl = { /** {@link Clock} class */ Clock: Clock, /** {@link ClockType} enum */ ClockType: ClockType, /** {@link Context} class */ Context: Context, /** * @constant {number} * The plus/minus tolerance for determining number equivalence. * * @see [FloatingPointRange]{@link FloatingPointRange} * @see [IntegerRange]{@link IntegerRange} */ DEFAULT_NUMERIC_RANGE_TOLERANCE: DEFAULT_NUMERIC_RANGE_TOLERANCE, /** {@link DistroUtils} */ DistroUtils: DistroUtils, /** {@link Duration} class */ Duration: Duration, /** {@link FloatingPointRange} class */ FloatingPointRange: FloatingPointRange, /** {@link IntegerRange} class */ IntegerRange: IntegerRange, /** {@link Logging} class */ logging: logging, /** {@link NodeOptions} class */ NodeOptions: NodeOptions, /** {@link Parameter} */ Parameter: Parameter, /** {@link ParameterDescriptor} */ ParameterDescriptor: ParameterDescriptor, /** {@link ParameterType} */ ParameterType: ParameterType, /** {@link QoS} class */ QoS: QoS, /** {@link RMWUtils} */ RMWUtils: RMWUtils, /** {@link ROSClock} class */ ROSClock: ROSClock, /** {@link ServiceIntrospectionStates} */ ServiceIntrospectionStates: ServiceIntrospectionStates, /** {@link Time} class */ Time: Time, /** {@link module:validator|validator} object */ validator: validator, /** {@link ActionClient} class */ ActionClient: ActionClient, /** {@link ActionServer} class */ ActionServer: ActionServer, /** {@link ActionUuid} class */ ActionUuid: ActionUuid, /** {@link ClientGoalHandle} class */ ClientGoalHandle: ClientGoalHandle, /** {@link ServerGoalHandle} class */ ServerGoalHandle: ServerGoalHandle, /** {@link ServerGoalHandle} enum */ CancelResponse: CancelResponse, /** {@link GoalResponse} enum */ GoalResponse: GoalResponse, /** {@link getActionClientNamesAndTypesByNode} function */ getActionClientNamesAndTypesByNode: getActionClientNamesAndTypesByNode, /** {@link getActionServerNamesAndTypesByNode} function */ getActionServerNamesAndTypesByNode: getActionServerNamesAndTypesByNode, /** {@link getActionNamesAndTypes} function */ getActionNamesAndTypes: getActionNamesAndTypes, /** {@link serializeMessage} function */ serializeMessage: serializeMessage, /** {@link deserializeMessage} function */ deserializeMessage: deserializeMessage, /** * Create and initialize a node. * @param {string} nodeName - The name used to register in ROS. * @param {string} [namespace=''] - The namespace used in ROS. * @param {Context} [context=Context.defaultContext()] - The context to create the node in. * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior. * @param {Array} [args=[]] - The arguments to pass to the node. * @param {boolean} [useGlobalArguments=true] - If true, the node will use the global arguments * from the context, otherwise it will only use the arguments * passed in the args parameter. * @return {Node} A new instance of the specified node. * @throws {Error} If the given context is not registered. * @deprecated since 0.18.0, Use new Node constructor. */ createNode( nodeName, namespace = '', context = Context.defaultContext(), options = NodeOptions.defaultOptions, args = [], useGlobalArguments = true ) { return new this.Node( nodeName, namespace, context, options, args, useGlobalArguments ); }, /** * Create a LifecycleNode, a managed Node that implements a well-defined life-cycle state * model using the {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}. * @param {string} nodeName - The name used to register in ROS. * @param {string} [namespace=''] - The namespace used in ROS. * @param {Context} [context=Context.defaultContext()] - The context to create the node in. * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior. * @param {boolean} [enableCommunicationsInterface=true] - enable lifecycle service interfaces, e.g., GetState. * @return {LifecycleNode} A new instance of the specified node. * @throws {Error} If the given context is not registered. * @deprecated since 0.18.0, Use new LifecycleNode constructor. */ createLifecycleNode( nodeName, namespace = '', context = Context.defaultContext(), options = NodeOptions.defaultOptions, enableCommunicationsInterface = true ) { return new this.lifecycle.LifecycleNode( nodeName, namespace, context, options, enableCommunicationsInterface ); }, /** * Initialize an RCL environment, i.e., a Context, and regenerate the javascript * message files if the output format of the message-generator tool has changed. * The context serves as a container for nodes, publishers, subscribers, etc. and * must be initialized before use. * @param {Context} [context=Context.defaultContext()] - The context to initialize. * @param {string[]} argv - Process command line arguments. * @return {Promise<undefined>} A Promise. * @throws {Error} If the given context has already been initialized or the command * line arguments argv could not be parsed. */ async init(context = Context.defaultContext(), argv = process.argv) { // check if context has already been initialized if (!context.isUninitialized()) { throw new Error('The context has already been initialized.'); } // check argv for correct value and state if (!Array.isArray(argv)) { throw new TypeError('argv must be an array.'); } if (!argv.every((argument) => typeof argument === 'string')) { throw new TypeError('argv elements must be strings (and not null).'); } rclnodejs.init(context.handle, argv, context._domainId); if (_rosVersionChecked) { // no further processing required return; } const version = await getCurrentGeneratorVersion(); const forced = version === null || compareVersions.compare(version, generator.version(), '<'); if (forced) { debug( 'The generator will begin to create JavaScript code from ROS IDL files...' ); } await generator.generateAll(forced); // TODO determine if tsd generateAll() should be here _rosVersionChecked = true; }, /** * Start detection and processing of units of work. * @param {Node} node - The node to be spun up. * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0. * @throws {Error} If the node is already spinning. * @return {undefined} * @deprecated since 0.18.0, Use Node.spin(timeout) */ spin(node, timeout = 10) { node.spin(timeout); }, /** * Spin the node and trigger the event loop to check for one incoming event. Thereafter the node * will not received additional events until running additional calls to spin() or spinOnce(). * @param {Node} node - The node to be spun. * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0. * @throws {Error} If the node is already spinning. * @return {undefined} * @deprecated since 0.18.0, Use Node.spinOnce(timeout) */ spinOnce(node, timeout = 10) { node.spinOnce(timeout); }, /** * Shutdown an RCL environment identified by a context. The shutdown process will * destroy all nodes and related resources in the context. If no context is * explicitly given, the default context will be shut down. * This follows the semantics of * [rclpy.shutdown()]{@link http://docs.ros2.org/latest/api/rclpy/api/init_shutdown.html#rclpy.shutdown}. * * @param { Context } [context = Context.defaultContext()] - The context to be shutdown. * @return { undefined } * @throws { Error } If there is a problem shutting down the context or while destroying or shutting down a node within it. */ shutdown(context = Context.defaultContext()) { context.shutdown(); }, /** * Shutdown all RCL environments via their contexts. * @return { undefined } * @throws { Error } If there is a problem shutting down the context or while destroying or shutting down a node within it. */ shutdownAll() { for (const context of Context.instances) { this.shutdown(context); } }, /** * Determine if an RCL environment identified by a context argument * has been shutdown. * @param {Context} [context=Context.defaultContext()] - The context to inspect. * @return {boolean} Return true if the module is shut down, otherwise return false. */ isShutdown(context = Context.defaultContext()) { return !context.isInitialized(); }, /** * Get the interface package, which is used by publisher/subscription or client/service. * @param {string} name - The name of interface to be required. * @return {object} - the object of the required package/interface. */ require(name) { return loader.loadInterface(name); }, /** * Search packages which locate under path $AMENT_PREFIX_PATH, regenerate all JavaScript structs files from the IDL of * messages(.msg) and services(.srv) and put these files under folder 'generated'. Any existing files under * this folder will be overwritten after the execution. * @return {Promise<undefined>} A Promise. */ async regenerateAll() { // This will trigger to regenerate all the JS structs used for messages and services, // to overwrite the existing ones although they have been created. debug('Begin regeneration of JavaScript code from ROS IDL files...'); // generate the messages and type declarations, which must be done in sequence await generator.generateAll(true); await tsdGenerator.generateAll(); // create interfaces.d.ts debug('Finished regeneration.'); }, /** * Judge if the topic/service is hidden (see [the ROS2 design documentation]{@link http://design.ros2.org/articles/topic_and_service_names.html#hidden-topic-or-service-names}). * @param {string} name - Name of topic/service. * @return {boolean} - True if a given topic or service name is hidden, otherwise False. */ isTopicOrServiceHidden(name) { if (typeof name !== 'string') { throw new TypeError('Invalid argument'); } return name.split('/').some((slice) => slice.startsWith('_')); }, /** * Expand a given topic name using given node name and namespace as well. * @param {string} topicName - Topic name to be expanded. * @param {string} nodeName - Name of the node that this topic is associated with. * @param {string} nodeNamespace - Namespace that the topic is within. * @return {string} Expanded topic name which is fully qualified. */ expandTopicName(topicName, nodeName, nodeNamespace) { return rclnodejs.expandTopicName(topicName, nodeName, nodeNamespace); }, createMessage(type) { const typeClass = loader.loadInterface(type); if (typeClass) { return new typeClass(); } return undefined; }, /** * Create a plain JavaScript from the specified type identifier. * @param {string|Object} type -- the type identifier, acceptable formats could be 'std_msgs/std/String' * or {package: 'std_msgs', type: 'msg', name: 'String'} * @return {Object|undefined} A plain JavaScript of that type, or undefined if the object could not be created */ createMessageObject(type) { return this.createMessage(type).toPlainObject(); }, /** * Removes the default signal handler installed by rclnodejs. After calling this, rclnodejs * will no longer clean itself up when a SIGINT is received, it is the application's * responsibility to properly shut down all nodes and contexts. * * Application which wishes to implement its own signal handler logic should call this. * @returns {undefined} */ removeSignalHandlers() { // 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 = () => { // shuts down all live contexts. Applications that wishes to use their own signal handlers // should call `rclnodejs.removeSignalHandlers`. debug('Catch ctrl+c event and will cleanup and terminate.'); rcl.shutdownAll(); }; process.on('SIGINT', _sigHandler); module.exports = rcl; // The following statements are located here to work around a // circular dependency issue occurring in rate.js. // Do not change the order of the following imports. const Node = require('./lib/node.js'); /** {@link Node} class */ rcl.Node = Node; const TimeSource = require('./lib/time_source.js'); /** {@link TimeSource} class */ rcl.TimeSource = TimeSource; const Lifecycle = require('./lib/lifecycle.js'); /** Lifecycle namespace */ rcl.lifecycle = Lifecycle;