diff --git a/lib/node.js b/lib/node.js index 5d3faeaf..e9a344f8 100644 --- a/lib/node.js +++ b/lib/node.js @@ -43,6 +43,7 @@ const TypeDescriptionService = require('./type_description_service.js'); const Entity = require('./entity.js'); const { SubscriptionEventCallbacks } = require('../lib/event_handler.js'); const { PublisherEventCallbacks } = require('../lib/event_handler.js'); +const { validateFullTopicName } = require('./validator.js'); // Parameter event publisher constants const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent'; @@ -1072,27 +1073,57 @@ class Node extends rclnodejs.ShadowNode { } /** - * Get a list of publishers on a given topic. - * @param {string} topic - the topic name to get the publishers for. - * @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name, - * otherwise it should be a valid ROS topic name. + * Return a list of publishers on a given topic. + * + * The returned parameter is a list of TopicEndpointInfo objects, where each will contain + * the node name, node namespace, topic type, topic endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `topic` should be a valid + * topic name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `topic` should + * follow ROS topic name conventions. + * + * `topic` may be a relative, private, or fully qualified topic name. + * A relative or private topic will be expanded using this node's namespace and name. + * The queried `topic` is not remapped. + * + * @param {string} topic - The topic on which to find the publishers. + * @param {boolean} [noDemangle=false] - If `true`, `topic` needs to be a valid middleware topic + * name, otherwise it should be a valid ROS topic name. Defaults to `false`. * @returns {Array} - list of publishers */ - getPublishersInfoByTopic(topic, noDemangle) { - return rclnodejs.getPublishersInfoByTopic(this.handle, topic, noDemangle); + getPublishersInfoByTopic(topic, noDemangle = false) { + return rclnodejs.getPublishersInfoByTopic( + this.handle, + this._getValidatedTopic(topic, noDemangle), + noDemangle + ); } /** - * Get a list of subscriptions on a given topic. - * @param {string} topic - the topic name to get the subscriptions for. - * @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name, - * otherwise it should be a valid ROS topic name. + * Return a list of subscriptions on a given topic. + * + * The returned parameter is a list of TopicEndpointInfo objects, where each will contain + * the node name, node namespace, topic type, topic endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `topic` should be a valid + * topic name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `topic` should + * follow ROS topic name conventions. + * + * `topic` may be a relative, private, or fully qualified topic name. + * A relative or private topic will be expanded using this node's namespace and name. + * The queried `topic` is not remapped. + * + * @param {string} topic - The topic on which to find the subscriptions. + * @param {boolean} [noDemangle=false] - If `true`, `topic` needs to be a valid middleware topic + name, otherwise it should be a valid ROS topic name. Defaults to `false`. * @returns {Array} - list of subscriptions */ - getSubscriptionsInfoByTopic(topic, noDemangle) { + getSubscriptionsInfoByTopic(topic, noDemangle = false) { return rclnodejs.getSubscriptionsInfoByTopic( this.handle, - topic, + this._getValidatedTopic(topic, noDemangle), noDemangle ); } @@ -1428,7 +1459,7 @@ class Node extends rclnodejs.ShadowNode { * Determine if a parameter descriptor exists. * * @param {string} name - The name of a descriptor to for. - * @return {boolean} - True if a descriptor has been declared; otherwise false. + * @return {boolean} - true if a descriptor has been declared; otherwise false. */ hasParameterDescriptor(name) { return !!this.getParameterDescriptor(name); @@ -1864,6 +1895,19 @@ class Node extends rclnodejs.ShadowNode { this._actionServers.push(actionServer); this.syncHandles(); } + + _getValidatedTopic(topicName, noDemangle) { + if (noDemangle) { + return topicName; + } + const fqTopicName = rclnodejs.expandTopicName( + topicName, + this.name(), + this.namespace() + ); + validateFullTopicName(fqTopicName); + return rclnodejs.remapTopicName(this.handle, fqTopicName); + } } /** diff --git a/src/rcl_node_bindings.cpp b/src/rcl_node_bindings.cpp index a231fee2..072e1dab 100644 --- a/src/rcl_node_bindings.cpp +++ b/src/rcl_node_bindings.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -507,6 +508,47 @@ Napi::Value ResolveName(const Napi::CallbackInfo& info) { return Napi::String::New(env, output_cstr); } +Napi::Value RemapTopicName(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + RclHandle* node_handle = RclHandle::Unwrap(info[0].As()); + rcl_node_t* node = reinterpret_cast(node_handle->ptr()); + std::string topic_name = info[1].As().Utf8Value(); + + const rcl_node_options_t* node_options = rcl_node_get_options(node); + if (nullptr == node_options) { + Napi::Error::New(env, "failed to get node options") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + + const rcl_arguments_t* global_args = nullptr; + if (node_options->use_global_arguments) { + global_args = &(node->context->global_arguments); + } + + char* output_cstr = nullptr; + rcl_ret_t ret = rcl_remap_topic_name( + &(node_options->arguments), global_args, topic_name.c_str(), + rcl_node_get_name(node), rcl_node_get_namespace(node), + node_options->allocator, &output_cstr); + if (RCL_RET_OK != ret) { + Napi::Error::New(env, "failed to remap topic name") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + if (nullptr == output_cstr) { + return Napi::String::New(env, topic_name); + } + + auto name_deleter = [&]() { + node_options->allocator.deallocate(output_cstr, + node_options->allocator.state); + }; + RCPPUTILS_SCOPE_EXIT({ name_deleter(); }); + + return Napi::String::New(env, output_cstr); +} + Napi::Object InitNodeBindings(Napi::Env env, Napi::Object exports) { exports.Set("getParameterOverrides", Napi::Function::New(env, GetParameterOverrides)); @@ -532,6 +574,7 @@ Napi::Object InitNodeBindings(Napi::Env env, Napi::Object exports) { exports.Set("getRMWImplementationIdentifier", Napi::Function::New(env, GetRMWImplementationIdentifier)); exports.Set("resolveName", Napi::Function::New(env, ResolveName)); + exports.Set("remapTopicName", Napi::Function::New(env, RemapTopicName)); return exports; } diff --git a/test/test-graph.js b/test/test-graph.js index 46030fd3..2484f5e7 100644 --- a/test/test-graph.js +++ b/test/test-graph.js @@ -29,21 +29,34 @@ describe('rclnodejs graph test suite', function () { }); it('Get publishers info by topic', function () { - const node = rclnodejs.createNode('publisher_node'); + const node = rclnodejs.createNode('publisher_node', '/my_ns'); + assert.deepStrictEqual( + 0, + node.getPublishersInfoByTopic('/my_ns/topic', false).length + ); const String = 'std_msgs/msg/String'; node.createPublisher(String, 'topic'); - const publishers = node.getPublishersInfoByTopic('/topic', false); + const publishers = node.getPublishersInfoByTopic('/my_ns/topic', false); assert.strictEqual(publishers.length, 1); + assert.strictEqual(publishers[0].node_namespace, '/my_ns'); assert.strictEqual(publishers[0].node_name, 'publisher_node'); assert.strictEqual(publishers[0].topic_type, String); }); it('Get subscriptions info by topic', function () { - const node = rclnodejs.createNode('subscription_node'); + const node = rclnodejs.createNode('subscription_node', '/my_ns'); + assert.deepStrictEqual( + 0, + node.getSubscriptionsInfoByTopic('/my_ns/topic', false).length + ); const String = 'std_msgs/msg/String'; node.createSubscription(String, 'topic', (msg) => {}); - const subscriptions = node.getSubscriptionsInfoByTopic('/topic', false); + const subscriptions = node.getSubscriptionsInfoByTopic( + '/my_ns/topic', + false + ); assert.strictEqual(subscriptions.length, 1); + assert.strictEqual(subscriptions[0].node_namespace, '/my_ns'); assert.strictEqual(subscriptions[0].node_name, 'subscription_node'); assert.strictEqual(subscriptions[0].topic_type, String); }); diff --git a/types/node.d.ts b/types/node.d.ts index 1e4c1353..e1d6459e 100644 --- a/types/node.d.ts +++ b/types/node.d.ts @@ -750,21 +750,45 @@ declare module 'rclnodejs' { getServiceNamesAndTypes(): Array; /** - * Get an array of publishers on a given topic. + * Return a list of publishers on a given topic. * - * @param topic - The name of the topic. - * @param noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name, - * otherwise it should be a valid ROS topic name. + * The returned parameter is a list of TopicEndpointInfo objects, where each will contain + * the node name, node namespace, topic type, topic endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `topic` should be a valid + * topic name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `topic` should + * follow ROS topic name conventions. + * + * `topic` may be a relative, private, or fully qualified topic name. + * A relative or private topic will be expanded using this node's namespace and name. + * The queried `topic` is not remapped. + * + * @param topic - The topic on which to find the publishers. + * @param [noDemangle=false] - If `true`, `topic` needs to be a valid middleware topic + * name, otherwise it should be a valid ROS topic name. Defaults to `false`. * @returns An array of publishers. */ getPublishersInfoByTopic(topic: string, noDemangle: boolean): Array; /** - * Get an array of subscriptions on a given topic. + * Return a list of subscriptions on a given topic. * - * @param topic - The name of the topic. - * @param noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name, - * otherwise it should be a valid ROS topic name. + * The returned parameter is a list of TopicEndpointInfo objects, where each will contain + * the node name, node namespace, topic type, topic endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `topic` should be a valid + * topic name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `topic` should + * follow ROS topic name conventions. + * + * `topic` may be a relative, private, or fully qualified topic name. + * A relative or private topic will be expanded using this node's namespace and name. + * The queried `topic` is not remapped. + * + * @param topic - The topic on which to find the subscriptions.. + * @param [noDemangle=false] - If `true`, `topic` needs to be a valid middleware topic + name, otherwise it should be a valid ROS topic name. Defaults to `false`. * @returns An array of subscriptions. */ getSubscriptionsInfoByTopic(