Skip to content

Commit 131ebf8

Browse files
authored
Add missing methods for graph (#1110)
This PR adds new graph-related methods to expose publisher and subscription information by topic. - Introduces native bindings and corresponding JavaScript wrappers for retrieving publishers and subscriptions info. - Adds test coverage in both TypeScript and JavaScript test files to verify the new functionality. - Updates build configuration and dependency lists to support the added functionality. Fix: #1111
1 parent 98d2b08 commit 131ebf8

File tree

11 files changed

+294
-47
lines changed

11 files changed

+294
-47
lines changed

binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
'-lrcl_lifecycle',
6666
'-lrcutils',
6767
'-lrcl_yaml_param_parser',
68+
'-lrcpputils',
6869
'-lrmw',
6970
'-lrosidl_runtime_c',
7071
],

lib/node.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,32 @@ class Node extends rclnodejs.ShadowNode {
10131013
return rclnodejs.getServiceNamesAndTypes(this.handle);
10141014
}
10151015

1016+
/**
1017+
* Get a list of publishers on a given topic.
1018+
* @param {string} topic - the topic name to get the publishers for.
1019+
* @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name,
1020+
* otherwise it should be a valid ROS topic name.
1021+
* @returns {Array} - list of publishers
1022+
*/
1023+
getPublishersInfoByTopic(topic, noDemangle) {
1024+
return rclnodejs.getPublishersInfoByTopic(this.handle, topic, noDemangle);
1025+
}
1026+
1027+
/**
1028+
* Get a list of subscriptions on a given topic.
1029+
* @param {string} topic - the topic name to get the subscriptions for.
1030+
* @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name,
1031+
* otherwise it should be a valid ROS topic name.
1032+
* @returns {Array} - list of subscriptions
1033+
*/
1034+
getSubscriptionsInfoByTopic(topic, noDemangle) {
1035+
return rclnodejs.getSubscriptionsInfoByTopic(
1036+
this.handle,
1037+
topic,
1038+
noDemangle
1039+
);
1040+
}
1041+
10161042
/**
10171043
* Get the list of nodes discovered by the provided node.
10181044
* @return {Array<string>} - An array of the names.

scripts/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const dependencies = [
2929
'builtin_interfaces',
3030
'rcl_lifecycle',
3131
'lifecycle_msgs',
32+
'rcpputils',
3233
'rosidl_runtime_c',
3334
'rosidl_dynamic_typesupport',
3435
'type_description_interfaces',

src/rcl_client_bindings.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,30 @@ Napi::Value GetClientServiceName(const Napi::CallbackInfo& info) {
112112
return Napi::String::New(env, service_name);
113113
}
114114

115+
Napi::Value ServiceServerIsAvailable(const Napi::CallbackInfo& info) {
116+
Napi::Env env = info.Env();
117+
118+
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
119+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
120+
RclHandle* client_handle = RclHandle::Unwrap(info[1].As<Napi::Object>());
121+
rcl_client_t* client = reinterpret_cast<rcl_client_t*>(client_handle->ptr());
122+
123+
bool is_available;
124+
THROW_ERROR_IF_NOT_EQUAL(
125+
RCL_RET_OK, rcl_service_server_is_available(node, client, &is_available),
126+
"Failed to get service state.");
127+
128+
return Napi::Boolean::New(env, is_available);
129+
}
130+
115131
Napi::Object InitClientBindings(Napi::Env env, Napi::Object exports) {
116132
exports.Set("createClient", Napi::Function::New(env, CreateClient));
117133
exports.Set("sendRequest", Napi::Function::New(env, SendRequest));
118134
exports.Set("rclTakeResponse", Napi::Function::New(env, RclTakeResponse));
119135
exports.Set("getClientServiceName",
120136
Napi::Function::New(env, GetClientServiceName));
137+
exports.Set("serviceServerIsAvailable",
138+
Napi::Function::New(env, ServiceServerIsAvailable));
121139
return exports;
122140
}
123141

src/rcl_graph_bindings.cpp

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#include <rcl/graph.h>
1919
#include <rcl/rcl.h>
2020

21+
#include <rcpputils/scope_exit.hpp>
22+
// NOLINTNEXTLINE
2123
#include <string>
2224

2325
#include "macros.h"
@@ -26,6 +28,11 @@
2628

2729
namespace rclnodejs {
2830

31+
typedef rcl_ret_t (*rcl_get_info_by_topic_func_t)(
32+
const rcl_node_t* node, rcutils_allocator_t* allocator,
33+
const char* topic_name, bool no_mangle,
34+
rcl_topic_endpoint_info_array_t* info_array);
35+
2936
Napi::Value GetPublisherNamesAndTypesByNode(const Napi::CallbackInfo& info) {
3037
Napi::Env env = info.Env();
3138

@@ -193,59 +200,61 @@ Napi::Value GetServiceNamesAndTypes(const Napi::CallbackInfo& info) {
193200
return result_list;
194201
}
195202

196-
Napi::Value GetNodeNames(const Napi::CallbackInfo& info) {
197-
Napi::Env env = info.Env();
198-
199-
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
200-
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
201-
rcutils_string_array_t node_names =
202-
rcutils_get_zero_initialized_string_array();
203-
rcutils_string_array_t node_namespaces =
204-
rcutils_get_zero_initialized_string_array();
205-
rcl_allocator_t allocator = rcl_get_default_allocator();
206-
207-
THROW_ERROR_IF_NOT_EQUAL(
208-
RCL_RET_OK,
209-
rcl_get_node_names(node, allocator, &node_names, &node_namespaces),
210-
"Failed to get_node_names.");
211-
212-
Napi::Array result_list = Napi::Array::New(env, node_names.size);
213-
214-
for (size_t i = 0; i < node_names.size; ++i) {
215-
Napi::Object item = Napi::Object::New(env);
216-
217-
item.Set("name", Napi::String::New(env, node_names.data[i]));
218-
item.Set("namespace", Napi::String::New(env, node_namespaces.data[i]));
219-
220-
result_list.Set(i, item);
203+
Napi::Value GetInfoByTopic(Napi::Env env, rcl_node_t* node,
204+
const char* topic_name, bool no_mangle,
205+
const char* type,
206+
rcl_get_info_by_topic_func_t rcl_get_info_by_topic) {
207+
rcutils_allocator_t allocator = rcutils_get_default_allocator();
208+
rcl_topic_endpoint_info_array_t info_array =
209+
rcl_get_zero_initialized_topic_endpoint_info_array();
210+
211+
RCPPUTILS_SCOPE_EXIT({
212+
rcl_ret_t fini_ret =
213+
rcl_topic_endpoint_info_array_fini(&info_array, &allocator);
214+
if (RCL_RET_OK != fini_ret) {
215+
Napi::Error::New(env, rcl_get_error_string().str)
216+
.ThrowAsJavaScriptException();
217+
rcl_reset_error();
218+
}
219+
});
220+
221+
rcl_ret_t ret = rcl_get_info_by_topic(node, &allocator, topic_name, no_mangle,
222+
&info_array);
223+
if (RCL_RET_OK != ret) {
224+
if (RCL_RET_UNSUPPORTED == ret) {
225+
Napi::Error::New(
226+
env, std::string("Failed to get information by topic for ") + type +
227+
": function not supported by RMW_IMPLEMENTATION")
228+
.ThrowAsJavaScriptException();
229+
return env.Undefined();
230+
}
231+
Napi::Error::New(
232+
env, std::string("Failed to get information by topic for ") + type)
233+
.ThrowAsJavaScriptException();
234+
return env.Undefined();
221235
}
222236

223-
rcutils_ret_t fini_names_ret = rcutils_string_array_fini(&node_names);
224-
rcutils_ret_t fini_namespaces_ret =
225-
rcutils_string_array_fini(&node_namespaces);
237+
return ConvertToJSTopicEndpointInfoList(env, &info_array);
238+
}
226239

227-
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, fini_names_ret,
228-
"Failed to destroy node_names");
229-
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, fini_namespaces_ret,
230-
"Failed to destroy node_namespaces");
240+
Napi::Value GetPublishersInfoByTopic(const Napi::CallbackInfo& info) {
241+
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
242+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
243+
std::string topic_name = info[1].As<Napi::String>().Utf8Value();
244+
bool no_mangle = info[2].As<Napi::Boolean>();
231245

232-
return result_list;
246+
return GetInfoByTopic(info.Env(), node, topic_name.c_str(), no_mangle,
247+
"publishers", rcl_get_publishers_info_by_topic);
233248
}
234249

235-
Napi::Value ServiceServerIsAvailable(const Napi::CallbackInfo& info) {
236-
Napi::Env env = info.Env();
237-
250+
Napi::Value GetSubscriptionsInfoByTopic(const Napi::CallbackInfo& info) {
238251
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
239252
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
240-
RclHandle* client_handle = RclHandle::Unwrap(info[1].As<Napi::Object>());
241-
rcl_client_t* client = reinterpret_cast<rcl_client_t*>(client_handle->ptr());
242-
243-
bool is_available;
244-
THROW_ERROR_IF_NOT_EQUAL(
245-
RCL_RET_OK, rcl_service_server_is_available(node, client, &is_available),
246-
"Failed to get service state.");
253+
std::string topic_name = info[1].As<Napi::String>().Utf8Value();
254+
bool no_mangle = info[2].As<Napi::Boolean>();
247255

248-
return Napi::Boolean::New(env, is_available);
256+
return GetInfoByTopic(info.Env(), node, topic_name.c_str(), no_mangle,
257+
"subscriptions", rcl_get_subscriptions_info_by_topic);
249258
}
250259

251260
Napi::Object InitGraphBindings(Napi::Env env, Napi::Object exports) {
@@ -261,9 +270,10 @@ Napi::Object InitGraphBindings(Napi::Env env, Napi::Object exports) {
261270
Napi::Function::New(env, GetTopicNamesAndTypes));
262271
exports.Set("getServiceNamesAndTypes",
263272
Napi::Function::New(env, GetServiceNamesAndTypes));
264-
exports.Set("getNodeNames", Napi::Function::New(env, GetNodeNames));
265-
exports.Set("serviceServerIsAvailable",
266-
Napi::Function::New(env, ServiceServerIsAvailable));
273+
exports.Set("getPublishersInfoByTopic",
274+
Napi::Function::New(env, GetPublishersInfoByTopic));
275+
exports.Set("getSubscriptionsInfoByTopic",
276+
Napi::Function::New(env, GetSubscriptionsInfoByTopic));
267277
return exports;
268278
}
269279

src/rcl_node_bindings.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,45 @@ Napi::Value CountServices(const Napi::CallbackInfo& info) {
378378
return Napi::Number::New(env, count);
379379
}
380380

381+
Napi::Value GetNodeNames(const Napi::CallbackInfo& info) {
382+
Napi::Env env = info.Env();
383+
384+
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
385+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
386+
rcutils_string_array_t node_names =
387+
rcutils_get_zero_initialized_string_array();
388+
rcutils_string_array_t node_namespaces =
389+
rcutils_get_zero_initialized_string_array();
390+
rcl_allocator_t allocator = rcl_get_default_allocator();
391+
392+
THROW_ERROR_IF_NOT_EQUAL(
393+
RCL_RET_OK,
394+
rcl_get_node_names(node, allocator, &node_names, &node_namespaces),
395+
"Failed to get_node_names.");
396+
397+
Napi::Array result_list = Napi::Array::New(env, node_names.size);
398+
399+
for (size_t i = 0; i < node_names.size; ++i) {
400+
Napi::Object item = Napi::Object::New(env);
401+
402+
item.Set("name", Napi::String::New(env, node_names.data[i]));
403+
item.Set("namespace", Napi::String::New(env, node_namespaces.data[i]));
404+
405+
result_list.Set(i, item);
406+
}
407+
408+
rcutils_ret_t fini_names_ret = rcutils_string_array_fini(&node_names);
409+
rcutils_ret_t fini_namespaces_ret =
410+
rcutils_string_array_fini(&node_namespaces);
411+
412+
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, fini_names_ret,
413+
"Failed to destroy node_names");
414+
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, fini_namespaces_ret,
415+
"Failed to destroy node_namespaces");
416+
417+
return result_list;
418+
}
419+
381420
Napi::Object InitNodeBindings(Napi::Env env, Napi::Object exports) {
382421
exports.Set("getParameterOverrides",
383422
Napi::Function::New(env, GetParameterOverrides));
@@ -395,6 +434,7 @@ Napi::Object InitNodeBindings(Napi::Env env, Napi::Object exports) {
395434
exports.Set("countSubscribers", Napi::Function::New(env, CountSubscribers));
396435
exports.Set("countClients", Napi::Function::New(env, CountClients));
397436
exports.Set("countServices", Napi::Function::New(env, CountServices));
437+
exports.Set("getNodeNames", Napi::Function::New(env, GetNodeNames));
398438
return exports;
399439
}
400440

src/rcl_utilities.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <rcl/rcl.h>
1818
#include <rcl_action/rcl_action.h>
19+
#include <rmw/topic_endpoint_info.h>
1920
#include <uv.h>
2021

2122
#include <memory>
@@ -69,6 +70,68 @@ std::unique_ptr<rmw_qos_profile_t> GetQosProfileFromObject(
6970
return qos_profile;
7071
}
7172

73+
Napi::Value ConvertRMWTimeToDuration(Napi::Env env,
74+
const rmw_time_t* duration) {
75+
Napi::Object obj = Napi::Object::New(env);
76+
obj.Set("seconds", Napi::BigInt::New(env, duration->sec));
77+
obj.Set("nanoseconds", Napi::Number::New(env, duration->nsec));
78+
return obj;
79+
}
80+
81+
Napi::Value ConvertToHashObject(Napi::Env env,
82+
const rosidl_type_hash_t* type_hash) {
83+
Napi::Object obj = Napi::Object::New(env);
84+
obj.Set("version", Napi::Number::New(env, type_hash->version));
85+
obj.Set("value", Napi::Buffer<char>::Copy(
86+
env, reinterpret_cast<const char*>(type_hash->value),
87+
ROSIDL_TYPE_HASH_SIZE));
88+
return obj;
89+
}
90+
91+
Napi::Value ConvertToQoS(Napi::Env env, const rmw_qos_profile_t* qos_profile) {
92+
Napi::Object qos = Napi::Object::New(env);
93+
qos.Set("depth", Napi::Number::New(env, qos_profile->depth));
94+
qos.Set("history", Napi::Number::New(env, qos_profile->history));
95+
qos.Set("reliability", Napi::Number::New(env, qos_profile->reliability));
96+
qos.Set("durability", Napi::Number::New(env, qos_profile->durability));
97+
qos.Set("lifespan", ConvertRMWTimeToDuration(env, &qos_profile->lifespan));
98+
qos.Set("deadline", ConvertRMWTimeToDuration(env, &qos_profile->deadline));
99+
qos.Set("liveliness", Napi::Number::New(env, qos_profile->liveliness));
100+
qos.Set(
101+
"liveliness_lease_duration",
102+
ConvertRMWTimeToDuration(env, &qos_profile->liveliness_lease_duration));
103+
qos.Set(
104+
"avoid_ros_namespace_conventions",
105+
Napi::Boolean::New(env, qos_profile->avoid_ros_namespace_conventions));
106+
return qos;
107+
}
108+
109+
Napi::Value ConvertToJSTopicEndpoint(
110+
Napi::Env env, const rmw_topic_endpoint_info_t* topic_endpoint_info) {
111+
Napi::Array endpoint_gid = Napi::Array::New(env, RMW_GID_STORAGE_SIZE);
112+
for (size_t i = 0; i < RMW_GID_STORAGE_SIZE; i++) {
113+
endpoint_gid.Set(
114+
i, Napi::Number::New(env, topic_endpoint_info->endpoint_gid[i]));
115+
}
116+
117+
Napi::Object endpoint = Napi::Object::New(env);
118+
endpoint.Set("node_name",
119+
Napi::String::New(env, topic_endpoint_info->node_name));
120+
endpoint.Set("node_namespace",
121+
Napi::String::New(env, topic_endpoint_info->node_namespace));
122+
endpoint.Set("topic_type",
123+
Napi::String::New(env, topic_endpoint_info->topic_type));
124+
endpoint.Set("topic_type_hash",
125+
ConvertToHashObject(env, &topic_endpoint_info->topic_type_hash));
126+
endpoint.Set("endpoint_type",
127+
Napi::Number::New(
128+
env, static_cast<int>(topic_endpoint_info->endpoint_type)));
129+
endpoint.Set("endpoint_gid", endpoint_gid);
130+
endpoint.Set("qos_profile",
131+
ConvertToQoS(env, &topic_endpoint_info->qos_profile));
132+
return endpoint;
133+
}
134+
72135
uv_lib_t g_lib;
73136
Napi::Env g_env = nullptr;
74137

@@ -183,4 +246,14 @@ void ExtractNamesAndTypes(rcl_names_and_types_t names_and_types,
183246
}
184247
}
185248

249+
Napi::Array ConvertToJSTopicEndpointInfoList(
250+
Napi::Env env, const rmw_topic_endpoint_info_array_t* info_array) {
251+
Napi::Array list = Napi::Array::New(env, info_array->size);
252+
for (size_t i = 0; i < info_array->size; ++i) {
253+
rmw_topic_endpoint_info_t topic_endpoint_info = info_array->info_array[i];
254+
list.Set(i, ConvertToJSTopicEndpoint(env, &topic_endpoint_info));
255+
}
256+
return list;
257+
}
258+
186259
} // namespace rclnodejs

src/rcl_utilities.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ std::unique_ptr<rmw_qos_profile_t> GetQoSProfile(Napi::Value qos);
4848
void ExtractNamesAndTypes(rcl_names_and_types_t names_and_types,
4949
Napi::Array* result_list);
5050

51+
Napi::Array ConvertToJSTopicEndpointInfoList(
52+
Napi::Env env, const rmw_topic_endpoint_info_array_t* info_array);
53+
5154
} // namespace rclnodejs
5255

5356
#endif // SRC_RCL_UTILITIES_H_

0 commit comments

Comments
 (0)