diff --git a/.github/workflows/linux-x64-build-and-test.yml b/.github/workflows/linux-x64-build-and-test.yml index a41fe8f4..c57f8eda 100644 --- a/.github/workflows/linux-x64-build-and-test.yml +++ b/.github/workflows/linux-x64-build-and-test.yml @@ -51,6 +51,7 @@ jobs: uses: ros-tooling/setup-ros@v0.7 with: required-ros-distributions: ${{ matrix.ros_distribution }} + use-ros2-testing: ${{ matrix.ros_distribution == 'rolling' }} - name: Install test-msgs on Linux run: | diff --git a/lib/node.js b/lib/node.js index 541fe258..ffe56c50 100644 --- a/lib/node.js +++ b/lib/node.js @@ -1349,6 +1349,74 @@ class Node extends rclnodejs.ShadowNode { ); } + /** + * Return a list of clients on a given service. + * + * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain + * the node name, node namespace, service type, service endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `service` should be a valid + * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `service` should + * follow ROS service name conventions. + * + * `service` may be a relative, private, or fully qualified service name. + * A relative or private service will be expanded using this node's namespace and name. + * The queried `service` is not remapped. + * + * @param {string} service - The service on which to find the clients. + * @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service + * name, otherwise it should be a valid ROS service name. Defaults to `false`. + * @returns {Array} - list of clients + */ + getClientsInfoByService(service, noDemangle = false) { + if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) { + console.warn( + 'getClientsInfoByService is not supported by this version of ROS 2' + ); + return null; + } + return rclnodejs.getClientsInfoByService( + this.handle, + this._getValidatedServiceName(service, noDemangle), + noDemangle + ); + } + + /** + * Return a list of servers on a given service. + * + * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain + * the node name, node namespace, service type, service endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `service` should be a valid + * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `service` should + * follow ROS service name conventions. + * + * `service` may be a relative, private, or fully qualified service name. + * A relative or private service will be expanded using this node's namespace and name. + * The queried `service` is not remapped. + * + * @param {string} service - The service on which to find the servers. + * @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service + * name, otherwise it should be a valid ROS service name. Defaults to `false`. + * @returns {Array} - list of servers + */ + getServersInfoByService(service, noDemangle = false) { + if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) { + console.warn( + 'getServersInfoByService is not supported by this version of ROS 2' + ); + return null; + } + return rclnodejs.getServersInfoByService( + this.handle, + this._getValidatedServiceName(service, noDemangle), + noDemangle + ); + } + /** * Get the list of nodes discovered by the provided node. * @return {Array} - An array of the names. @@ -2142,6 +2210,22 @@ class Node extends rclnodejs.ShadowNode { validateFullTopicName(fqTopicName); return rclnodejs.remapTopicName(this.handle, fqTopicName); } + + _getValidatedServiceName(serviceName, noDemangle) { + if (typeof serviceName !== 'string') { + throw new TypeValidationError('serviceName', serviceName, 'string', { + nodeName: this.name(), + }); + } + + if (noDemangle) { + return serviceName; + } + + const resolvedServiceName = this.resolveServiceName(serviceName); + rclnodejs.validateTopicName(resolvedServiceName); + return resolvedServiceName; + } } /** diff --git a/src/rcl_graph_bindings.cpp b/src/rcl_graph_bindings.cpp index 476b561d..f825a31c 100644 --- a/src/rcl_graph_bindings.cpp +++ b/src/rcl_graph_bindings.cpp @@ -33,6 +33,13 @@ typedef rcl_ret_t (*rcl_get_info_by_topic_func_t)( const char* topic_name, bool no_mangle, rcl_topic_endpoint_info_array_t* info_array); +#if ROS_VERSION > 2505 +typedef rcl_ret_t (*rcl_get_info_by_service_func_t)( + const rcl_node_t* node, rcutils_allocator_t* allocator, + const char* service_name, bool no_mangle, + rcl_service_endpoint_info_array_t* info_array); +#endif // ROS_VERSION > 2505 + Napi::Value GetPublisherNamesAndTypesByNode(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); @@ -257,6 +264,66 @@ Napi::Value GetSubscriptionsInfoByTopic(const Napi::CallbackInfo& info) { "subscriptions", rcl_get_subscriptions_info_by_topic); } +#if ROS_VERSION > 2505 +Napi::Value GetInfoByService( + Napi::Env env, rcl_node_t* node, const char* service_name, bool no_mangle, + const char* type, rcl_get_info_by_service_func_t rcl_get_info_by_service) { + rcutils_allocator_t allocator = rcutils_get_default_allocator(); + rcl_service_endpoint_info_array_t info_array = + rcl_get_zero_initialized_service_endpoint_info_array(); + + RCPPUTILS_SCOPE_EXIT({ + rcl_ret_t fini_ret = + rcl_service_endpoint_info_array_fini(&info_array, &allocator); + if (RCL_RET_OK != fini_ret) { + Napi::Error::New(env, rcl_get_error_string().str) + .ThrowAsJavaScriptException(); + rcl_reset_error(); + } + }); + + rcl_ret_t ret = rcl_get_info_by_service(node, &allocator, service_name, + no_mangle, &info_array); + if (RCL_RET_OK != ret) { + if (RCL_RET_UNSUPPORTED == ret) { + Napi::Error::New( + env, std::string("Failed to get information by service for ") + type + + ": function not supported by RMW_IMPLEMENTATION") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + Napi::Error::New( + env, std::string("Failed to get information by service for ") + type) + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + + return ConvertToJSServiceEndpointInfoList(env, &info_array); +} +#endif // ROS_VERSION > 2505 + +#if ROS_VERSION > 2505 +Napi::Value GetClientsInfoByService(const Napi::CallbackInfo& info) { + RclHandle* node_handle = RclHandle::Unwrap(info[0].As()); + rcl_node_t* node = reinterpret_cast(node_handle->ptr()); + std::string service_name = info[1].As().Utf8Value(); + bool no_mangle = info[2].As(); + + return GetInfoByService(info.Env(), node, service_name.c_str(), no_mangle, + "clients", rcl_get_clients_info_by_service); +} + +Napi::Value GetServersInfoByService(const Napi::CallbackInfo& info) { + RclHandle* node_handle = RclHandle::Unwrap(info[0].As()); + rcl_node_t* node = reinterpret_cast(node_handle->ptr()); + std::string service_name = info[1].As().Utf8Value(); + bool no_mangle = info[2].As(); + + return GetInfoByService(info.Env(), node, service_name.c_str(), no_mangle, + "servers", rcl_get_servers_info_by_service); +} +#endif // ROS_VERSION > 2505 + Napi::Object InitGraphBindings(Napi::Env env, Napi::Object exports) { exports.Set("getPublisherNamesAndTypesByNode", Napi::Function::New(env, GetPublisherNamesAndTypesByNode)); @@ -274,6 +341,12 @@ Napi::Object InitGraphBindings(Napi::Env env, Napi::Object exports) { Napi::Function::New(env, GetPublishersInfoByTopic)); exports.Set("getSubscriptionsInfoByTopic", Napi::Function::New(env, GetSubscriptionsInfoByTopic)); +#if ROS_VERSION > 2505 + exports.Set("getClientsInfoByService", + Napi::Function::New(env, GetClientsInfoByService)); + exports.Set("getServersInfoByService", + Napi::Function::New(env, GetServersInfoByService)); +#endif // ROS_VERSION > 2505 return exports; } diff --git a/src/rcl_utilities.cpp b/src/rcl_utilities.cpp index 977c86f4..bfefd547 100644 --- a/src/rcl_utilities.cpp +++ b/src/rcl_utilities.cpp @@ -119,6 +119,48 @@ Napi::Value ConvertToJSTopicEndpoint( return endpoint; } +#if ROS_VERSION > 2505 +Napi::Value ConvertToJSServiceEndpointInfo( + Napi::Env env, const rmw_service_endpoint_info_t* service_endpoint_info) { + Napi::Object endpoint = Napi::Object::New(env); + endpoint.Set("node_name", + Napi::String::New(env, service_endpoint_info->node_name)); + endpoint.Set("node_namespace", + Napi::String::New(env, service_endpoint_info->node_namespace)); + endpoint.Set("service_type", + Napi::String::New(env, service_endpoint_info->service_type)); + endpoint.Set( + "service_type_hash", + ConvertToHashObject(env, &service_endpoint_info->service_type_hash)); + endpoint.Set( + "endpoint_type", + Napi::Number::New( + env, static_cast(service_endpoint_info->endpoint_type))); + endpoint.Set("endpoint_count", + Napi::Number::New(env, service_endpoint_info->endpoint_count)); + + Napi::Array endpoint_gids = + Napi::Array::New(env, service_endpoint_info->endpoint_count); + Napi::Array qos_profiles = + Napi::Array::New(env, service_endpoint_info->endpoint_count); + + for (size_t i = 0; i < service_endpoint_info->endpoint_count; i++) { + Napi::Array gid = Napi::Array::New(env, RMW_GID_STORAGE_SIZE); + for (size_t j = 0; j < RMW_GID_STORAGE_SIZE; j++) { + gid.Set(j, Napi::Number::New(env, + service_endpoint_info->endpoint_gids[i][j])); + } + endpoint_gids.Set(i, gid); + qos_profiles.Set(i, rclnodejs::ConvertToQoS( + env, &service_endpoint_info->qos_profiles[i])); + } + endpoint.Set("endpoint_gids", endpoint_gids); + endpoint.Set("qos_profiles", qos_profiles); + + return endpoint; +} +#endif // ROS_VERSION > 2505 + uv_lib_t g_lib; Napi::Env g_env = nullptr; @@ -261,6 +303,19 @@ Napi::Array ConvertToJSTopicEndpointInfoList( return list; } +#if ROS_VERSION > 2505 +Napi::Array ConvertToJSServiceEndpointInfoList( + Napi::Env env, const rmw_service_endpoint_info_array_t* info_array) { + Napi::Array list = Napi::Array::New(env, info_array->size); + for (size_t i = 0; i < info_array->size; ++i) { + rmw_service_endpoint_info_t service_endpoint_info = + info_array->info_array[i]; + list.Set(i, ConvertToJSServiceEndpointInfo(env, &service_endpoint_info)); + } + return list; +} +#endif // ROS_VERSION > 2505 + char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv) { size_t argc = jsArgv.Length(); char** argv = nullptr; diff --git a/src/rcl_utilities.h b/src/rcl_utilities.h index 5dde11ae..5f46c8ff 100644 --- a/src/rcl_utilities.h +++ b/src/rcl_utilities.h @@ -51,6 +51,11 @@ void ExtractNamesAndTypes(rcl_names_and_types_t names_and_types, Napi::Array ConvertToJSTopicEndpointInfoList( Napi::Env env, const rmw_topic_endpoint_info_array_t* info_array); +#if ROS_VERSION > 2505 +Napi::Array ConvertToJSServiceEndpointInfoList( + Napi::Env env, const rmw_service_endpoint_info_array_t* info_array); +#endif // ROS_VERSION > 2505 + Napi::Value ConvertToQoS(Napi::Env env, const rmw_qos_profile_t* qos_profile); // `AbstractArgsFromNapiArray` and `FreeArgs` must be called in pairs. diff --git a/test/test-graph.js b/test/test-graph.js index 2484f5e7..189c879a 100644 --- a/test/test-graph.js +++ b/test/test-graph.js @@ -60,4 +60,48 @@ describe('rclnodejs graph test suite', function () { assert.strictEqual(subscriptions[0].node_name, 'subscription_node'); assert.strictEqual(subscriptions[0].topic_type, String); }); + + it('Get clients info by service', function () { + if ( + rclnodejs.DistroUtils.getDistroId() < + rclnodejs.DistroUtils.DistroId.ROLLING + ) { + this.skip(); + } + + const node = rclnodejs.createNode('client_node', '/my_ns'); + assert.deepStrictEqual( + 0, + node.getClientsInfoByService('/my_ns/service', false).length + ); + const AddTwoInts = 'example_interfaces/srv/AddTwoInts'; + node.createClient(AddTwoInts, 'service'); + const clients = node.getClientsInfoByService('/my_ns/service', false); + assert.strictEqual(clients.length, 1); + assert.strictEqual(clients[0].node_namespace, '/my_ns'); + assert.strictEqual(clients[0].node_name, 'client_node'); + assert.strictEqual(clients[0].service_type, AddTwoInts); + }); + + it('Get servers info by service', function () { + if ( + rclnodejs.DistroUtils.getDistroId() < + rclnodejs.DistroUtils.DistroId.ROLLING + ) { + this.skip(); + } + + const node = rclnodejs.createNode('server_node', '/my_ns'); + assert.deepStrictEqual( + 0, + node.getServersInfoByService('/my_ns/service', false).length + ); + const AddTwoInts = 'example_interfaces/srv/AddTwoInts'; + node.createService(AddTwoInts, 'service', (req, res) => {}); + const servers = node.getServersInfoByService('/my_ns/service', false); + assert.strictEqual(servers.length, 1); + assert.strictEqual(servers[0].node_namespace, '/my_ns'); + assert.strictEqual(servers[0].node_name, 'server_node'); + assert.strictEqual(servers[0].service_type, AddTwoInts); + }); }); diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index b2e0a408..be3702c7 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -80,6 +80,8 @@ expectType( ); expectType>(node.getPublishersInfoByTopic('topic', false)); expectType>(node.getSubscriptionsInfoByTopic('topic', false)); +expectType>(node.getClientsInfoByService('service', false)); +expectType>(node.getServersInfoByService('service', false)); expectType(node.countPublishers(TOPIC)); expectType(node.countSubscribers(TOPIC)); expectType(node.countClients(SERVICE_NAME)); diff --git a/types/node.d.ts b/types/node.d.ts index 1a7dd2ef..6fd2cb1c 100644 --- a/types/node.d.ts +++ b/types/node.d.ts @@ -851,6 +851,56 @@ declare module 'rclnodejs' { noDemangle: boolean ): Array; + /** + * Return a list of clients on a given service. + * + * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain + * the node name, node namespace, service type, service endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `service` should be a valid + * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `service` should + * follow ROS service name conventions. + * + * `service` may be a relative, private, or fully qualified service name. + * A relative or private service will be expanded using this node's namespace and name. + * The queried `service` is not remapped. + * + * @param service - The service on which to find the clients. + * @param [noDemangle=false] - If `true`, `service` needs to be a valid middleware service + * name, otherwise it should be a valid ROS service name. Defaults to `false`. + * @returns An array of clients. + */ + getClientsInfoByService( + service: string, + noDemangle: boolean + ): Array; + + /** + * Return a list of servers on a given service. + * + * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain + * the node name, node namespace, service type, service endpoint's GID, and its QoS profile. + * + * When the `no_mangle` parameter is `true`, the provided `service` should be a valid + * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS) + * apps). When the `no_mangle` parameter is `false`, the provided `service` should + * follow ROS service name conventions. + * + * `service` may be a relative, private, or fully qualified service name. + * A relative or private service will be expanded using this node's namespace and name. + * The queried `service` is not remapped. + * + * @param service - The service on which to find the servers. + * @param [noDemangle=false] - If `true`, `service` needs to be a valid middleware service + * name, otherwise it should be a valid ROS service name. Defaults to `false`. + * @returns An array of servers. + */ + getServersInfoByService( + service: string, + noDemangle: boolean + ): Array; + /** * Get the list of nodes discovered by the provided node. *