Skip to content

Commit fe3e866

Browse files
committed
Add getServersInfoByService and getClientsInfoByService for node
1 parent cd267c5 commit fe3e866

7 files changed

Lines changed: 309 additions & 0 deletions

File tree

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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ 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+
typedef rcl_ret_t (*rcl_get_info_by_service_func_t)(
37+
const rcl_node_t* node, rcutils_allocator_t* allocator,
38+
const char* service_name, bool no_mangle,
39+
rcl_service_endpoint_info_array_t* info_array);
40+
3641
Napi::Value GetPublisherNamesAndTypesByNode(const Napi::CallbackInfo& info) {
3742
Napi::Env env = info.Env();
3843

@@ -257,6 +262,64 @@ Napi::Value GetSubscriptionsInfoByTopic(const Napi::CallbackInfo& info) {
257262
"subscriptions", rcl_get_subscriptions_info_by_topic);
258263
}
259264

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

src/rcl_utilities.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,48 @@ Napi::Value ConvertToJSTopicEndpoint(
119119
return endpoint;
120120
}
121121

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

@@ -261,6 +303,19 @@ Napi::Array ConvertToJSTopicEndpointInfoList(
261303
return list;
262304
}
263305

306+
#if ROS_VERSION > 2405
307+
Napi::Array ConvertToJSServiceEndpointInfoList(
308+
Napi::Env env, const rmw_service_endpoint_info_array_t* info_array) {
309+
Napi::Array list = Napi::Array::New(env, info_array->size);
310+
for (size_t i = 0; i < info_array->size; ++i) {
311+
rmw_service_endpoint_info_t service_endpoint_info =
312+
info_array->info_array[i];
313+
list.Set(i, ConvertToJSServiceEndpointInfo(env, &service_endpoint_info));
314+
}
315+
return list;
316+
}
317+
#endif
318+
264319
char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv) {
265320
size_t argc = jsArgv.Length();
266321
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 > 2405
55+
Napi::Array ConvertToJSServiceEndpointInfoList(
56+
Napi::Env env, const rmw_service_endpoint_info_array_t* info_array);
57+
#endif
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)