Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/linux-x64-build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
84 changes: 84 additions & 0 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version check is inconsistent with the C++ code. The C++ code uses #if ROS_VERSION > 2405 (greater than JAZZY), which includes KILTED (2505) and ROLLING (5000). However, this JavaScript check < DistroUtils.DistroId.ROLLING only includes ROLLING and excludes KILTED, causing a mismatch.

For KILTED (2505):

  • C++ code will compile the functions (2505 > 2405 = true)
  • JavaScript code will return null (2505 < 5000 = true)

The check should be <= DistroUtils.DistroId.JAZZY to match the C++ version check.

Suggested change
if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
if (DistroUtils.getDistroId() <= DistroUtils.DistroId.JAZZY) {

Copilot uses AI. Check for mistakes.
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) {
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version check is inconsistent with the C++ code. The C++ code uses #if ROS_VERSION > 2405 (greater than JAZZY), which includes KILTED (2505) and ROLLING (5000). However, this JavaScript check < DistroUtils.DistroId.ROLLING only includes ROLLING and excludes KILTED, causing a mismatch.

For KILTED (2505):

  • C++ code will compile the functions (2505 > 2405 = true)
  • JavaScript code will return null (2505 < 5000 = true)

The check should be <= DistroUtils.DistroId.JAZZY to match the C++ version check.

Copilot uses AI. Check for mistakes.
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<string>} - An array of the names.
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
73 changes: 73 additions & 0 deletions src/rcl_graph_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}
Comment on lines +268 to +302
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GetInfoByService function uses types and functions (e.g., rcl_service_endpoint_info_array_t, rcl_get_zero_initialized_service_endpoint_info_array(), rcl_service_endpoint_info_array_fini()) that only exist in ROS versions > 2405. This entire function should be guarded with #if ROS_VERSION > 2405 and #endif to prevent compilation errors on older ROS versions.

Copilot uses AI. Check for mistakes.
#endif // ROS_VERSION > 2505

#if ROS_VERSION > 2505
Napi::Value GetClientsInfoByService(const Napi::CallbackInfo& info) {
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
std::string service_name = info[1].As<Napi::String>().Utf8Value();
bool no_mangle = info[2].As<Napi::Boolean>();

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<Napi::Object>());
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
std::string service_name = info[1].As<Napi::String>().Utf8Value();
bool no_mangle = info[2].As<Napi::Boolean>();

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));
Expand All @@ -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;
}

Expand Down
55 changes: 55 additions & 0 deletions src/rcl_utilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(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;

Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/rcl_utilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 44 additions & 0 deletions test/test-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +66 to +67
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version check is inconsistent with the C++ code. The C++ code uses #if ROS_VERSION > 2405 (greater than JAZZY), which includes KILTED (2505) and ROLLING (5000). However, this test skip condition < DistroUtils.DistroId.ROLLING only includes ROLLING and excludes KILTED.

For KILTED (2505):

  • C++ code will compile the functions (2505 > 2405 = true)
  • Test will be skipped (2505 < 5000 = true)

The check should be <= DistroUtils.DistroId.JAZZY to match the C++ version check.

Suggested change
rclnodejs.DistroUtils.getDistroId() <
rclnodejs.DistroUtils.DistroId.ROLLING
rclnodejs.DistroUtils.getDistroId() <=
rclnodejs.DistroUtils.DistroId.JAZZY

Copilot uses AI. Check for mistakes.
) {
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
Comment on lines +88 to +89
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version check is inconsistent with the C++ code. The C++ code uses #if ROS_VERSION > 2405 (greater than JAZZY), which includes KILTED (2505) and ROLLING (5000). However, this test skip condition < DistroUtils.DistroId.ROLLING only includes ROLLING and excludes KILTED.

For KILTED (2505):

  • C++ code will compile the functions (2505 > 2405 = true)
  • Test will be skipped (2505 < 5000 = true)

The check should be <= DistroUtils.DistroId.JAZZY to match the C++ version check.

Copilot uses AI. Check for mistakes.
) {
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);
});
});
2 changes: 2 additions & 0 deletions test/types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ expectType<rclnodejs.NodeNamesQueryResultWithEnclaves[]>(
);
expectType<Array<object>>(node.getPublishersInfoByTopic('topic', false));
expectType<Array<object>>(node.getSubscriptionsInfoByTopic('topic', false));
expectType<Array<object>>(node.getClientsInfoByService('service', false));
expectType<Array<object>>(node.getServersInfoByService('service', false));
expectType<number>(node.countPublishers(TOPIC));
expectType<number>(node.countSubscribers(TOPIC));
expectType<number>(node.countClients(SERVICE_NAME));
Expand Down
50 changes: 50 additions & 0 deletions types/node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,56 @@ declare module 'rclnodejs' {
noDemangle: boolean
): Array<object>;

/**
* 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<object>;

/**
* 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<object>;

/**
* Get the list of nodes discovered by the provided node.
*
Expand Down
Loading