Skip to content

Commit 2221b4a

Browse files
authored
Add getServersInfoByService and getClientsInfoByService for node (#1335)
This PR adds two new methods to the Node class for querying service endpoint information: `getClientsInfoByService` and `getServersInfoByService`. These methods mirror the existing `getPublishersInfoByTopic` and `getSubscriptionsInfoByTopic` functionality but for ROS services instead of topics. - Adds `getClientsInfoByService()` and `getServersInfoByService()` methods to query service endpoint information - Implements C++ bindings using new ROS 2 service endpoint info APIs - Includes TypeScript type definitions and test coverage for the new methods Fix: #1330
1 parent 21453ac commit 2221b4a

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed

.github/workflows/linux-x64-build-and-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
uses: ros-tooling/[email protected]
5252
with:
5353
required-ros-distributions: ${{ matrix.ros_distribution }}
54+
use-ros2-testing: ${{ matrix.ros_distribution == 'rolling' }}
5455

5556
- name: Install test-msgs on Linux
5657
run: |

lib/node.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,74 @@ class Node extends rclnodejs.ShadowNode {
13491349
);
13501350
}
13511351

1352+
/**
1353+
* Return a list of clients on a given service.
1354+
*
1355+
* The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
1356+
* the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
1357+
*
1358+
* When the `no_mangle` parameter is `true`, the provided `service` should be a valid
1359+
* service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1360+
* apps). When the `no_mangle` parameter is `false`, the provided `service` should
1361+
* follow ROS service name conventions.
1362+
*
1363+
* `service` may be a relative, private, or fully qualified service name.
1364+
* A relative or private service will be expanded using this node's namespace and name.
1365+
* The queried `service` is not remapped.
1366+
*
1367+
* @param {string} service - The service on which to find the clients.
1368+
* @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
1369+
* name, otherwise it should be a valid ROS service name. Defaults to `false`.
1370+
* @returns {Array} - list of clients
1371+
*/
1372+
getClientsInfoByService(service, noDemangle = false) {
1373+
if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
1374+
console.warn(
1375+
'getClientsInfoByService is not supported by this version of ROS 2'
1376+
);
1377+
return null;
1378+
}
1379+
return rclnodejs.getClientsInfoByService(
1380+
this.handle,
1381+
this._getValidatedServiceName(service, noDemangle),
1382+
noDemangle
1383+
);
1384+
}
1385+
1386+
/**
1387+
* Return a list of servers on a given service.
1388+
*
1389+
* The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
1390+
* the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
1391+
*
1392+
* When the `no_mangle` parameter is `true`, the provided `service` should be a valid
1393+
* service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1394+
* apps). When the `no_mangle` parameter is `false`, the provided `service` should
1395+
* follow ROS service name conventions.
1396+
*
1397+
* `service` may be a relative, private, or fully qualified service name.
1398+
* A relative or private service will be expanded using this node's namespace and name.
1399+
* The queried `service` is not remapped.
1400+
*
1401+
* @param {string} service - The service on which to find the servers.
1402+
* @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
1403+
* name, otherwise it should be a valid ROS service name. Defaults to `false`.
1404+
* @returns {Array} - list of servers
1405+
*/
1406+
getServersInfoByService(service, noDemangle = false) {
1407+
if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
1408+
console.warn(
1409+
'getServersInfoByService is not supported by this version of ROS 2'
1410+
);
1411+
return null;
1412+
}
1413+
return rclnodejs.getServersInfoByService(
1414+
this.handle,
1415+
this._getValidatedServiceName(service, noDemangle),
1416+
noDemangle
1417+
);
1418+
}
1419+
13521420
/**
13531421
* Get the list of nodes discovered by the provided node.
13541422
* @return {Array<string>} - An array of the names.
@@ -2142,6 +2210,22 @@ class Node extends rclnodejs.ShadowNode {
21422210
validateFullTopicName(fqTopicName);
21432211
return rclnodejs.remapTopicName(this.handle, fqTopicName);
21442212
}
2213+
2214+
_getValidatedServiceName(serviceName, noDemangle) {
2215+
if (typeof serviceName !== 'string') {
2216+
throw new TypeValidationError('serviceName', serviceName, 'string', {
2217+
nodeName: this.name(),
2218+
});
2219+
}
2220+
2221+
if (noDemangle) {
2222+
return serviceName;
2223+
}
2224+
2225+
const resolvedServiceName = this.resolveServiceName(serviceName);
2226+
rclnodejs.validateTopicName(resolvedServiceName);
2227+
return resolvedServiceName;
2228+
}
21452229
}
21462230

21472231
/**

src/rcl_graph_bindings.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ typedef rcl_ret_t (*rcl_get_info_by_topic_func_t)(
3333
const char* topic_name, bool no_mangle,
3434
rcl_topic_endpoint_info_array_t* info_array);
3535

36+
#if ROS_VERSION > 2505
37+
typedef rcl_ret_t (*rcl_get_info_by_service_func_t)(
38+
const rcl_node_t* node, rcutils_allocator_t* allocator,
39+
const char* service_name, bool no_mangle,
40+
rcl_service_endpoint_info_array_t* info_array);
41+
#endif // ROS_VERSION > 2505
42+
3643
Napi::Value GetPublisherNamesAndTypesByNode(const Napi::CallbackInfo& info) {
3744
Napi::Env env = info.Env();
3845

@@ -257,6 +264,66 @@ Napi::Value GetSubscriptionsInfoByTopic(const Napi::CallbackInfo& info) {
257264
"subscriptions", rcl_get_subscriptions_info_by_topic);
258265
}
259266

267+
#if ROS_VERSION > 2505
268+
Napi::Value GetInfoByService(
269+
Napi::Env env, rcl_node_t* node, const char* service_name, bool no_mangle,
270+
const char* type, rcl_get_info_by_service_func_t rcl_get_info_by_service) {
271+
rcutils_allocator_t allocator = rcutils_get_default_allocator();
272+
rcl_service_endpoint_info_array_t info_array =
273+
rcl_get_zero_initialized_service_endpoint_info_array();
274+
275+
RCPPUTILS_SCOPE_EXIT({
276+
rcl_ret_t fini_ret =
277+
rcl_service_endpoint_info_array_fini(&info_array, &allocator);
278+
if (RCL_RET_OK != fini_ret) {
279+
Napi::Error::New(env, rcl_get_error_string().str)
280+
.ThrowAsJavaScriptException();
281+
rcl_reset_error();
282+
}
283+
});
284+
285+
rcl_ret_t ret = rcl_get_info_by_service(node, &allocator, service_name,
286+
no_mangle, &info_array);
287+
if (RCL_RET_OK != ret) {
288+
if (RCL_RET_UNSUPPORTED == ret) {
289+
Napi::Error::New(
290+
env, std::string("Failed to get information by service for ") + type +
291+
": function not supported by RMW_IMPLEMENTATION")
292+
.ThrowAsJavaScriptException();
293+
return env.Undefined();
294+
}
295+
Napi::Error::New(
296+
env, std::string("Failed to get information by service for ") + type)
297+
.ThrowAsJavaScriptException();
298+
return env.Undefined();
299+
}
300+
301+
return ConvertToJSServiceEndpointInfoList(env, &info_array);
302+
}
303+
#endif // ROS_VERSION > 2505
304+
305+
#if ROS_VERSION > 2505
306+
Napi::Value GetClientsInfoByService(const Napi::CallbackInfo& info) {
307+
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
308+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
309+
std::string service_name = info[1].As<Napi::String>().Utf8Value();
310+
bool no_mangle = info[2].As<Napi::Boolean>();
311+
312+
return GetInfoByService(info.Env(), node, service_name.c_str(), no_mangle,
313+
"clients", rcl_get_clients_info_by_service);
314+
}
315+
316+
Napi::Value GetServersInfoByService(const Napi::CallbackInfo& info) {
317+
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
318+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
319+
std::string service_name = info[1].As<Napi::String>().Utf8Value();
320+
bool no_mangle = info[2].As<Napi::Boolean>();
321+
322+
return GetInfoByService(info.Env(), node, service_name.c_str(), no_mangle,
323+
"servers", rcl_get_servers_info_by_service);
324+
}
325+
#endif // ROS_VERSION > 2505
326+
260327
Napi::Object InitGraphBindings(Napi::Env env, Napi::Object exports) {
261328
exports.Set("getPublisherNamesAndTypesByNode",
262329
Napi::Function::New(env, GetPublisherNamesAndTypesByNode));
@@ -274,6 +341,12 @@ Napi::Object InitGraphBindings(Napi::Env env, Napi::Object exports) {
274341
Napi::Function::New(env, GetPublishersInfoByTopic));
275342
exports.Set("getSubscriptionsInfoByTopic",
276343
Napi::Function::New(env, GetSubscriptionsInfoByTopic));
344+
#if ROS_VERSION > 2505
345+
exports.Set("getClientsInfoByService",
346+
Napi::Function::New(env, GetClientsInfoByService));
347+
exports.Set("getServersInfoByService",
348+
Napi::Function::New(env, GetServersInfoByService));
349+
#endif // ROS_VERSION > 2505
277350
return exports;
278351
}
279352

src/rcl_utilities.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,48 @@ Napi::Value ConvertToJSTopicEndpoint(
122122
return endpoint;
123123
}
124124

125+
#if ROS_VERSION > 2505
126+
Napi::Value ConvertToJSServiceEndpointInfo(
127+
Napi::Env env, const rmw_service_endpoint_info_t* service_endpoint_info) {
128+
Napi::Object endpoint = Napi::Object::New(env);
129+
endpoint.Set("node_name",
130+
Napi::String::New(env, service_endpoint_info->node_name));
131+
endpoint.Set("node_namespace",
132+
Napi::String::New(env, service_endpoint_info->node_namespace));
133+
endpoint.Set("service_type",
134+
Napi::String::New(env, service_endpoint_info->service_type));
135+
endpoint.Set(
136+
"service_type_hash",
137+
ConvertToHashObject(env, &service_endpoint_info->service_type_hash));
138+
endpoint.Set(
139+
"endpoint_type",
140+
Napi::Number::New(
141+
env, static_cast<int>(service_endpoint_info->endpoint_type)));
142+
endpoint.Set("endpoint_count",
143+
Napi::Number::New(env, service_endpoint_info->endpoint_count));
144+
145+
Napi::Array endpoint_gids =
146+
Napi::Array::New(env, service_endpoint_info->endpoint_count);
147+
Napi::Array qos_profiles =
148+
Napi::Array::New(env, service_endpoint_info->endpoint_count);
149+
150+
for (size_t i = 0; i < service_endpoint_info->endpoint_count; i++) {
151+
Napi::Array gid = Napi::Array::New(env, RMW_GID_STORAGE_SIZE);
152+
for (size_t j = 0; j < RMW_GID_STORAGE_SIZE; j++) {
153+
gid.Set(j, Napi::Number::New(env,
154+
service_endpoint_info->endpoint_gids[i][j]));
155+
}
156+
endpoint_gids.Set(i, gid);
157+
qos_profiles.Set(i, rclnodejs::ConvertToQoS(
158+
env, &service_endpoint_info->qos_profiles[i]));
159+
}
160+
endpoint.Set("endpoint_gids", endpoint_gids);
161+
endpoint.Set("qos_profiles", qos_profiles);
162+
163+
return endpoint;
164+
}
165+
#endif // ROS_VERSION > 2505
166+
125167
uv_lib_t g_lib;
126168
Napi::Env g_env = nullptr;
127169

@@ -264,6 +306,19 @@ Napi::Array ConvertToJSTopicEndpointInfoList(
264306
return list;
265307
}
266308

309+
#if ROS_VERSION > 2505
310+
Napi::Array ConvertToJSServiceEndpointInfoList(
311+
Napi::Env env, const rmw_service_endpoint_info_array_t* info_array) {
312+
Napi::Array list = Napi::Array::New(env, info_array->size);
313+
for (size_t i = 0; i < info_array->size; ++i) {
314+
rmw_service_endpoint_info_t service_endpoint_info =
315+
info_array->info_array[i];
316+
list.Set(i, ConvertToJSServiceEndpointInfo(env, &service_endpoint_info));
317+
}
318+
return list;
319+
}
320+
#endif // ROS_VERSION > 2505
321+
267322
char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv) {
268323
size_t argc = jsArgv.Length();
269324
char** argv = nullptr;

src/rcl_utilities.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ void ExtractNamesAndTypes(rcl_names_and_types_t names_and_types,
5151
Napi::Array ConvertToJSTopicEndpointInfoList(
5252
Napi::Env env, const rmw_topic_endpoint_info_array_t* info_array);
5353

54+
#if ROS_VERSION > 2505
55+
Napi::Array ConvertToJSServiceEndpointInfoList(
56+
Napi::Env env, const rmw_service_endpoint_info_array_t* info_array);
57+
#endif // ROS_VERSION > 2505
58+
5459
Napi::Value ConvertToQoS(Napi::Env env, const rmw_qos_profile_t* qos_profile);
5560

5661
// `AbstractArgsFromNapiArray` and `FreeArgs` must be called in pairs.

test/test-graph.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,48 @@ describe('rclnodejs graph test suite', function () {
6060
assert.strictEqual(subscriptions[0].node_name, 'subscription_node');
6161
assert.strictEqual(subscriptions[0].topic_type, String);
6262
});
63+
64+
it('Get clients info by service', function () {
65+
if (
66+
rclnodejs.DistroUtils.getDistroId() <
67+
rclnodejs.DistroUtils.DistroId.ROLLING
68+
) {
69+
this.skip();
70+
}
71+
72+
const node = rclnodejs.createNode('client_node', '/my_ns');
73+
assert.deepStrictEqual(
74+
0,
75+
node.getClientsInfoByService('/my_ns/service', false).length
76+
);
77+
const AddTwoInts = 'example_interfaces/srv/AddTwoInts';
78+
node.createClient(AddTwoInts, 'service');
79+
const clients = node.getClientsInfoByService('/my_ns/service', false);
80+
assert.strictEqual(clients.length, 1);
81+
assert.strictEqual(clients[0].node_namespace, '/my_ns');
82+
assert.strictEqual(clients[0].node_name, 'client_node');
83+
assert.strictEqual(clients[0].service_type, AddTwoInts);
84+
});
85+
86+
it('Get servers info by service', function () {
87+
if (
88+
rclnodejs.DistroUtils.getDistroId() <
89+
rclnodejs.DistroUtils.DistroId.ROLLING
90+
) {
91+
this.skip();
92+
}
93+
94+
const node = rclnodejs.createNode('server_node', '/my_ns');
95+
assert.deepStrictEqual(
96+
0,
97+
node.getServersInfoByService('/my_ns/service', false).length
98+
);
99+
const AddTwoInts = 'example_interfaces/srv/AddTwoInts';
100+
node.createService(AddTwoInts, 'service', (req, res) => {});
101+
const servers = node.getServersInfoByService('/my_ns/service', false);
102+
assert.strictEqual(servers.length, 1);
103+
assert.strictEqual(servers[0].node_namespace, '/my_ns');
104+
assert.strictEqual(servers[0].node_name, 'server_node');
105+
assert.strictEqual(servers[0].service_type, AddTwoInts);
106+
});
63107
});

test/types/index.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ expectType<rclnodejs.NodeNamesQueryResultWithEnclaves[]>(
8080
);
8181
expectType<Array<object>>(node.getPublishersInfoByTopic('topic', false));
8282
expectType<Array<object>>(node.getSubscriptionsInfoByTopic('topic', false));
83+
expectType<Array<object>>(node.getClientsInfoByService('service', false));
84+
expectType<Array<object>>(node.getServersInfoByService('service', false));
8385
expectType<number>(node.countPublishers(TOPIC));
8486
expectType<number>(node.countSubscribers(TOPIC));
8587
expectType<number>(node.countClients(SERVICE_NAME));

types/node.d.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,56 @@ declare module 'rclnodejs' {
851851
noDemangle: boolean
852852
): Array<object>;
853853

854+
/**
855+
* Return a list of clients on a given service.
856+
*
857+
* The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
858+
* the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
859+
*
860+
* When the `no_mangle` parameter is `true`, the provided `service` should be a valid
861+
* service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
862+
* apps). When the `no_mangle` parameter is `false`, the provided `service` should
863+
* follow ROS service name conventions.
864+
*
865+
* `service` may be a relative, private, or fully qualified service name.
866+
* A relative or private service will be expanded using this node's namespace and name.
867+
* The queried `service` is not remapped.
868+
*
869+
* @param service - The service on which to find the clients.
870+
* @param [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
871+
* name, otherwise it should be a valid ROS service name. Defaults to `false`.
872+
* @returns An array of clients.
873+
*/
874+
getClientsInfoByService(
875+
service: string,
876+
noDemangle: boolean
877+
): Array<object>;
878+
879+
/**
880+
* Return a list of servers on a given service.
881+
*
882+
* The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
883+
* the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
884+
*
885+
* When the `no_mangle` parameter is `true`, the provided `service` should be a valid
886+
* service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
887+
* apps). When the `no_mangle` parameter is `false`, the provided `service` should
888+
* follow ROS service name conventions.
889+
*
890+
* `service` may be a relative, private, or fully qualified service name.
891+
* A relative or private service will be expanded using this node's namespace and name.
892+
* The queried `service` is not remapped.
893+
*
894+
* @param service - The service on which to find the servers.
895+
* @param [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
896+
* name, otherwise it should be a valid ROS service name. Defaults to `false`.
897+
* @returns An array of servers.
898+
*/
899+
getServersInfoByService(
900+
service: string,
901+
noDemangle: boolean
902+
): Array<object>;
903+
854904
/**
855905
* Get the list of nodes discovered by the provided node.
856906
*

0 commit comments

Comments
 (0)