Skip to content

Commit 345393c

Browse files
authored
Support type description service (#1146)
This PR adds support for the ROS 2 Type Description Service, including native bindings, JavaScript API, test coverage, build integration, and an option to start the service automatically. - Introduces C++ N-API bindings for the type description service and integrates them in the module initialization. - Implements a JavaScript `TypeDescriptionService` class, updates `NodeOptions`/`Node` to optionally start the service, and adds tests. - Updates `binding.gyp` to include the new source and extends test files to handle distro version differences. See doc: https://docs.ros.org/en/jazzy/Releases/Release-Jazzy-Jalisco.html#add-get-type-description-service Fix: #1143
1 parent 8249941 commit 345393c

9 files changed

+308
-4
lines changed

binding.gyp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@
166166
]
167167
}
168168
],
169+
[
170+
# After Humble, e.g., Jazzy, Kilted.
171+
'ros_version > 2205', {
172+
'sources': [
173+
'./src/rcl_type_description_service_bindings.cpp',
174+
]
175+
}
176+
],
169177
[
170178
'runtime=="electron"', {
171179
"defines": ["NODE_RUNTIME_ELECTRON=1"]

lib/node.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const Service = require('./service.js');
3939
const Subscription = require('./subscription.js');
4040
const TimeSource = require('./time_source.js');
4141
const Timer = require('./timer.js');
42+
const TypeDescriptionService = require('./type_description_service.js');
4243
const Entity = require('./entity.js');
4344

4445
// Parameter event publisher constants
@@ -101,6 +102,7 @@ class Node extends rclnodejs.ShadowNode {
101102
this._parameterDescriptors = new Map();
102103
this._parameters = new Map();
103104
this._parameterService = null;
105+
this._typeDescriptionService = null;
104106
this._parameterEventPublisher = null;
105107
this._setParametersCallbacks = [];
106108
this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
@@ -147,6 +149,14 @@ class Node extends rclnodejs.ShadowNode {
147149
this._parameterService = new ParameterService(this);
148150
this._parameterService.start();
149151
}
152+
153+
if (
154+
DistroUtils.getDistroId() >= DistroUtils.getDistroId('jazzy') &&
155+
options.startTypeDescriptionService
156+
) {
157+
this._typeDescriptionService = new TypeDescriptionService(this);
158+
this._typeDescriptionService.start();
159+
}
150160
}
151161

152162
execute(handles) {

lib/node_options.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@ class NodeOptions {
2626
* @param {boolean} [startParameterServices=true]
2727
* @param {array} [parameterOverrides=[]]
2828
* @param {boolean} [automaticallyDeclareParametersFromOverrides=false]
29+
* @param {boolean} [startTypeDescriptionService=true]
2930
*/
3031
constructor(
3132
startParameterServices = true,
3233
parameterOverrides = [],
33-
automaticallyDeclareParametersFromOverrides = false
34+
automaticallyDeclareParametersFromOverrides = false,
35+
startTypeDescriptionService = true
3436
) {
3537
this._startParameterServices = startParameterServices;
3638
this._parameterOverrides = parameterOverrides;
3739
this._automaticallyDeclareParametersFromOverrides =
3840
automaticallyDeclareParametersFromOverrides;
41+
this._startTypeDescriptionService = startTypeDescriptionService;
3942
}
4043

4144
/**
@@ -105,6 +108,23 @@ class NodeOptions {
105108
this._automaticallyDeclareParametersFromOverrides = declareParamsFlag;
106109
}
107110

111+
/**
112+
* Get the startTypeDescriptionService option, only available for ROS2 > Humble.
113+
* Default value = true;
114+
* @returns {boolean} - true if the type description service is enabled.
115+
*/
116+
get startTypeDescriptionService() {
117+
return this._startTypeDescriptionService;
118+
}
119+
120+
/**
121+
* Set startTypeDescriptionService, only available for ROS2 > Humble
122+
* @param {boolean} willStartTypeDescriptionService
123+
*/
124+
set startTypeDescriptionService(willStartTypeDescriptionService) {
125+
this._startTypeDescriptionService = willStartTypeDescriptionService;
126+
}
127+
108128
/**
109129
* Return an instance configured with default options.
110130
* @returns {NodeOptions} - An instance with default values.

lib/type_description_service.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) 2025, The Robot Web Tools Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const loader = require('./interface_loader.js');
18+
const rclnodejs = require('bindings')('rclnodejs');
19+
const Service = require('./service.js');
20+
21+
// This class is used to create a TypeDescriptionService which can be used to
22+
// retrieve information about types used by the node’s publishers, subscribers,
23+
// services or actions.
24+
class TypeDescriptionService {
25+
constructor(node) {
26+
this._node = node;
27+
this._serviceName = this._node.name() + '/get_type_description';
28+
this._typeDescriptionServiceHandle = rclnodejs.initTypeDescriptionService(
29+
this._node.handle
30+
);
31+
this._typeClass = loader.loadInterface(
32+
'type_description_interfaces/srv/GetTypeDescription'
33+
);
34+
this._typeDescriptionService = null;
35+
}
36+
37+
start() {
38+
if (this._typeDescriptionService) {
39+
return;
40+
}
41+
42+
this._typeDescriptionService = new Service(
43+
this._node.handle,
44+
this._typeDescriptionServiceHandle,
45+
this._serviceName,
46+
this._typeClass,
47+
this._node._validateOptions(undefined),
48+
(request, response) => {
49+
const responseToBeSent = new this._typeClass.Response();
50+
const requestReceived = new this._typeClass.Request(request);
51+
rclnodejs.handleRequest(
52+
this._node.handle,
53+
requestReceived.serialize(),
54+
responseToBeSent.serialize()
55+
);
56+
responseToBeSent.deserialize(responseToBeSent.refObject);
57+
rclnodejs.sendResponse(
58+
this._typeDescriptionServiceHandle,
59+
responseToBeSent.serialize(),
60+
response._header
61+
);
62+
return null;
63+
}
64+
);
65+
this._node._services.push(this._typeDescriptionService);
66+
this._node.syncHandles();
67+
}
68+
69+
/**
70+
* Get the node this
71+
* @return {Node} - The supported node.
72+
*/
73+
get node() {
74+
return this._node;
75+
}
76+
77+
static toTypeHash(topicTypeHash) {
78+
return `RIHS0${topicTypeHash.version}_${topicTypeHash.value.toString('hex')}`;
79+
}
80+
}
81+
82+
module.exports = TypeDescriptionService;

src/addon.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
#include "rcl_subscription_bindings.h"
3535
#include "rcl_time_point_bindings.h"
3636
#include "rcl_timer_bindings.h"
37+
#if ROS_VERSION > 2205 // ROS2 > Humble
38+
#include "rcl_type_description_service_bindings.h"
39+
#endif
3740
#include "rcl_utilities.h"
3841
#include "shadow_node.h"
3942

@@ -79,6 +82,9 @@ Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
7982
rclnodejs::InitSubscriptionBindings(env, exports);
8083
rclnodejs::InitTimePointBindings(env, exports);
8184
rclnodejs::InitTimerBindings(env, exports);
85+
#if ROS_VERSION > 2205 // ROS2 > Humble
86+
rclnodejs::InitTypeDescriptionServiceBindings(env, exports);
87+
#endif
8288
rclnodejs::InitLifecycleBindings(env, exports);
8389
rclnodejs::ShadowNode::Init(env, exports);
8490
rclnodejs::RclHandle::Init(env, exports);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2025, The Robot Web Tools Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "rcl_type_description_service_bindings.h"
16+
17+
#include <napi.h>
18+
#include <rcl/rcl.h>
19+
#include <rmw/types.h>
20+
21+
#include "rcl_handle.h"
22+
23+
namespace rclnodejs {
24+
25+
Napi::Value InitTypeDescriptionService(const Napi::CallbackInfo& info) {
26+
Napi::Env env = info.Env();
27+
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
28+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
29+
rcl_service_t* service =
30+
reinterpret_cast<rcl_service_t*>(malloc(sizeof(rcl_service_t)));
31+
*service = rcl_get_zero_initialized_service();
32+
rcl_ret_t ret = rcl_node_type_description_service_init(service, node);
33+
if (RCL_RET_OK != ret) {
34+
Napi::Error::New(env, "Failed to initialize type description service")
35+
.ThrowAsJavaScriptException();
36+
}
37+
38+
auto service_handle =
39+
RclHandle::NewInstance(env, service, node_handle, [node, env](void* ptr) {
40+
rcl_service_t* service = reinterpret_cast<rcl_service_t*>(ptr);
41+
rcl_ret_t ret = rcl_service_fini(service, node);
42+
if (RCL_RET_OK != ret) {
43+
Napi::Error::New(env, "Failed to destroy type description service")
44+
.ThrowAsJavaScriptException();
45+
}
46+
free(ptr);
47+
});
48+
return service_handle;
49+
}
50+
51+
Napi::Value HandleRequest(const Napi::CallbackInfo& info) {
52+
Napi::Env env = info.Env();
53+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(
54+
RclHandle::Unwrap(info[0].As<Napi::Object>())->ptr());
55+
void* request = info[1].As<Napi::Buffer<char>>().Data();
56+
void* taken_response = info[2].As<Napi::Buffer<char>>().Data();
57+
58+
rmw_request_id_t header;
59+
rcl_node_type_description_service_handle_request(
60+
node, &header,
61+
static_cast<
62+
type_description_interfaces__srv__GetTypeDescription_Request*>(
63+
request),
64+
static_cast<
65+
type_description_interfaces__srv__GetTypeDescription_Response*>(
66+
taken_response));
67+
return env.Undefined();
68+
}
69+
70+
Napi::Object InitTypeDescriptionServiceBindings(Napi::Env env,
71+
Napi::Object exports) {
72+
exports.Set("handleRequest", Napi::Function::New(env, HandleRequest));
73+
exports.Set("initTypeDescriptionService",
74+
Napi::Function::New(env, InitTypeDescriptionService));
75+
76+
return exports;
77+
}
78+
79+
} // namespace rclnodejs
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2025, The Robot Web Tools Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef SRC_RCL_TYPE_DESCRIPTION_SERVICE_BINDINGS_H_
16+
#define SRC_RCL_TYPE_DESCRIPTION_SERVICE_BINDINGS_H_
17+
18+
#include <napi.h>
19+
20+
namespace rclnodejs {
21+
22+
Napi::Object InitTypeDescriptionServiceBindings(Napi::Env env,
23+
Napi::Object exports);
24+
25+
}
26+
27+
#endif // SRC_RCL_TYPE_DESCRIPTION_SERVICE_BINDINGS_H_

test/test-extra-destroy-methods.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const assert = require('assert');
1818
const rclnodejs = require('../index.js');
1919
const assertUtils = require('./utils.js');
2020
const assertThrowsError = assertUtils.assertThrowsError;
21+
const DistroUtils = require('../lib/distro.js');
2122

2223
describe('Node extra destroy methods testing', function () {
2324
before(function () {
@@ -88,9 +89,9 @@ describe('Node extra destroy methods testing', function () {
8889
it('destroyService()', function () {
8990
var node = rclnodejs.createNode('node4');
9091
const AddTwoInts = 'example_interfaces/srv/AddTwoInts';
92+
const initialServiceCount = node._services.length;
9193
var service = node.createService(AddTwoInts, 'add_two_ints', () => {});
92-
assert.deepStrictEqual(node._services.length, 7);
93-
94+
assert.deepStrictEqual(node._services.length, initialServiceCount + 1);
9495
assertThrowsError(
9596
function () {
9697
node.destroyService('service');
@@ -101,7 +102,7 @@ describe('Node extra destroy methods testing', function () {
101102
);
102103

103104
node.destroyService(service);
104-
assert.deepStrictEqual(node._services.length, 6);
105+
assert.deepStrictEqual(node._services.length, initialServiceCount);
105106
});
106107

107108
it('destroyTimer()', function () {

0 commit comments

Comments
 (0)