// 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 { 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 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');
/**
* 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);
}
});
}
});
});
}
/**
* A module that exposes the rclnodejs interfaces.
* @exports rclnodejs
*/
let rcl = {
_rosVersionChecked: false,
/** {@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 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 ROSClock} class */
ROSClock: ROSClock,
/** {@link Time} class */
Time: Time,
/** {@link module:validator|validator} object */
validator: validator,
/** {@link ActionClient} class */
ActionClient: ActionClient,
/** {@link ActionServer} class */
ActionServer: ActionServer,
/** {@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,
/**
* 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.
* @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
) {
return new this.Node(nodeName, namespace, context, options);
},
/**
* 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);
if (this._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
this._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);
},
};
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;