Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@
]
}
],
[
# After Humble, e.g., Jazzy, Kilted.
'ros_version > 2205', {
Copy link

Copilot AI May 23, 2025

Choose a reason for hiding this comment

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

The GYP condition uses ros_version while the code uses ROS_VERSION. Ensure the variable names and casing align so the new source is correctly included during build.

Copilot uses AI. Check for mistakes.

'sources': [
'./src/rcl_type_description_service_bindings.cpp',
]
}
],
[
'runtime=="electron"', {
"defines": ["NODE_RUNTIME_ELECTRON=1"]
Expand Down
10 changes: 10 additions & 0 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const Service = require('./service.js');
const Subscription = require('./subscription.js');
const TimeSource = require('./time_source.js');
const Timer = require('./timer.js');
const TypeDescriptionService = require('./type_description_service.js');
const Entity = require('./entity.js');

// Parameter event publisher constants
Expand Down Expand Up @@ -101,6 +102,7 @@ class Node extends rclnodejs.ShadowNode {
this._parameterDescriptors = new Map();
this._parameters = new Map();
this._parameterService = null;
this._typeDescriptionService = null;
this._parameterEventPublisher = null;
this._setParametersCallbacks = [];
this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
Expand Down Expand Up @@ -147,6 +149,14 @@ class Node extends rclnodejs.ShadowNode {
this._parameterService = new ParameterService(this);
this._parameterService.start();
}

if (
DistroUtils.getDistroId() >= DistroUtils.getDistroId('jazzy') &&
options.startTypeDescriptionService
) {
this._typeDescriptionService = new TypeDescriptionService(this);
this._typeDescriptionService.start();
}
}

execute(handles) {
Expand Down
22 changes: 21 additions & 1 deletion lib/node_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,19 @@ class NodeOptions {
* @param {boolean} [startParameterServices=true]
* @param {array} [parameterOverrides=[]]
* @param {boolean} [automaticallyDeclareParametersFromOverrides=false]
* @param {boolean} [startTypeDescriptionService=true]
*/
constructor(
startParameterServices = true,
parameterOverrides = [],
automaticallyDeclareParametersFromOverrides = false
automaticallyDeclareParametersFromOverrides = false,
startTypeDescriptionService = true
) {
this._startParameterServices = startParameterServices;
this._parameterOverrides = parameterOverrides;
this._automaticallyDeclareParametersFromOverrides =
automaticallyDeclareParametersFromOverrides;
this._startTypeDescriptionService = startTypeDescriptionService;
}

/**
Expand Down Expand Up @@ -105,6 +108,23 @@ class NodeOptions {
this._automaticallyDeclareParametersFromOverrides = declareParamsFlag;
}

/**
* Get the startTypeDescriptionService option.
* Default value = true;
* @returns {boolean} -
*/
get startTypeDescriptionService() {
return this._startTypeDescriptionService;
}

/**
* Set startTypeDescriptionService.
* @param {boolean} willStartTypeDescriptionService
*/
set startTypeDescriptionService(willStartTypeDescriptionService) {
this._startTypeDescriptionService = willStartTypeDescriptionService;
}

/**
* Return an instance configured with default options.
* @returns {NodeOptions} - An instance with default values.
Expand Down
82 changes: 82 additions & 0 deletions lib/type_description_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2025, The Robot Web Tools Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

const loader = require('./interface_loader.js');
const rclnodejs = require('bindings')('rclnodejs');
const Service = require('./service.js');

// This class is used to create a TypeDescriptionService which can be used to
// retrieve information about types used by the node’s publishers, subscribers,
// services or actions.
class TypeDescriptionService {
constructor(node) {
this._node = node;
this._serviceName = this._node.name() + '/get_type_description';
this._typeDescriptionServiceHandle = rclnodejs.initTypeDescriptionService(
this._node.handle
);
this._typeClass = loader.loadInterface(
'type_description_interfaces/srv/GetTypeDescription'
);
this._typeDescriptionService = null;
}

start() {
if (this._typeDescriptionService) {
return;
}

this._typeDescriptionService = new Service(
this._node.handle,
this._typeDescriptionServiceHandle,
this._serviceName,
this._typeClass,
this._node._validateOptions(undefined),
(request, response) => {
const responseToBeSent = new this._typeClass.Response();
const requestReceived = new this._typeClass.Request(request);
rclnodejs.handleRequest(
this._node.handle,
requestReceived.serialize(),
responseToBeSent.serialize()
);
responseToBeSent.deserialize(responseToBeSent.refObject);
rclnodejs.sendResponse(
this._typeDescriptionServiceHandle,
responseToBeSent.serialize(),
response._header
);
return null;
}
);
this._node._services.push(this._typeDescriptionService);
this._node.syncHandles();
}

/**
* Get the node this
* @return {Node} - The supported node.
*/
get node() {
return this._node;
}

static toTypeHash(topicTypeHash) {
return `RIHS0${topicTypeHash.version}_${topicTypeHash.value.toString('hex')}`;
}
}

module.exports = TypeDescriptionService;
6 changes: 6 additions & 0 deletions src/addon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
#include "rcl_subscription_bindings.h"
#include "rcl_time_point_bindings.h"
#include "rcl_timer_bindings.h"
#if ROS_VERSION > 2205 // ROS2 > Humble
#include "rcl_type_description_service_bindings.h"
#endif
#include "rcl_utilities.h"
#include "shadow_node.h"

Expand Down Expand Up @@ -79,6 +82,9 @@ Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
rclnodejs::InitSubscriptionBindings(env, exports);
rclnodejs::InitTimePointBindings(env, exports);
rclnodejs::InitTimerBindings(env, exports);
#if ROS_VERSION > 2205 // ROS2 > Humble
rclnodejs::InitTypeDescriptionServiceBindings(env, exports);
#endif
rclnodejs::InitLifecycleBindings(env, exports);
rclnodejs::ShadowNode::Init(env, exports);
rclnodejs::RclHandle::Init(env, exports);
Expand Down
79 changes: 79 additions & 0 deletions src/rcl_type_description_service_bindings.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2025, The Robot Web Tools Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "rcl_type_description_service_bindings.h"

#include <napi.h>
#include <rcl/rcl.h>
#include <rmw/types.h>

#include "rcl_handle.h"

namespace rclnodejs {

Napi::Value InitTypeDescriptionService(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
RclHandle* node_handle = RclHandle::Unwrap(info[0].As<Napi::Object>());
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(node_handle->ptr());
rcl_service_t* service =
reinterpret_cast<rcl_service_t*>(malloc(sizeof(rcl_service_t)));
*service = rcl_get_zero_initialized_service();
rcl_ret_t ret = rcl_node_type_description_service_init(service, node);
if (RCL_RET_OK != ret) {
Napi::Error::New(env, "Failed to initialize type description service")
.ThrowAsJavaScriptException();
}

auto service_handle =
RclHandle::NewInstance(env, service, node_handle, [node, env](void* ptr) {
rcl_service_t* service = reinterpret_cast<rcl_service_t*>(ptr);
rcl_ret_t ret = rcl_service_fini(service, node);
if (RCL_RET_OK != ret) {
Napi::Error::New(env, "Failed to destroy type description service")
.ThrowAsJavaScriptException();
}
free(ptr);
});
return service_handle;
}

Napi::Value HandleRequest(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(
RclHandle::Unwrap(info[0].As<Napi::Object>())->ptr());
void* request = info[1].As<Napi::Buffer<char>>().Data();
void* taken_response = info[2].As<Napi::Buffer<char>>().Data();

rmw_request_id_t header;
rcl_node_type_description_service_handle_request(
node, &header,
static_cast<
type_description_interfaces__srv__GetTypeDescription_Request*>(
request),
static_cast<
type_description_interfaces__srv__GetTypeDescription_Response*>(
taken_response));
return env.Undefined();
}

Napi::Object InitTypeDescriptionServiceBindings(Napi::Env env,
Napi::Object exports) {
exports.Set("handleRequest", Napi::Function::New(env, HandleRequest));
exports.Set("initTypeDescriptionService",
Napi::Function::New(env, InitTypeDescriptionService));

return exports;
}

} // namespace rclnodejs
27 changes: 27 additions & 0 deletions src/rcl_type_description_service_bindings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2025, The Robot Web Tools Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef SRC_RCL_TYPE_DESCRIPTION_SERVICE_BINDINGS_H_
#define SRC_RCL_TYPE_DESCRIPTION_SERVICE_BINDINGS_H_

#include <napi.h>

namespace rclnodejs {

Napi::Object InitTypeDescriptionServiceBindings(Napi::Env env,
Napi::Object exports);

}

#endif // SRC_RCL_TYPE_DESCRIPTION_SERVICE_BINDINGS_H_
13 changes: 11 additions & 2 deletions test/test-extra-destroy-methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const assert = require('assert');
const rclnodejs = require('../index.js');
const assertUtils = require('./utils.js');
const assertThrowsError = assertUtils.assertThrowsError;
const DistroUtils = require('../lib/distro.js');

describe('Node extra destroy methods testing', function () {
before(function () {
Expand Down Expand Up @@ -89,7 +90,11 @@ describe('Node extra destroy methods testing', function () {
var node = rclnodejs.createNode('node4');
const AddTwoInts = 'example_interfaces/srv/AddTwoInts';
var service = node.createService(AddTwoInts, 'add_two_ints', () => {});
assert.deepStrictEqual(node._services.length, 7);
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
assert.deepStrictEqual(node._services.length, 7);
} else {
assert.deepStrictEqual(node._services.length, 8);
}
Copy link

Copilot AI May 23, 2025

Choose a reason for hiding this comment

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

[nitpick] Using hardcoded expected counts (7 and 8) for _services.length may break if other services change. Consider deriving the expected count dynamically or add a comment explaining why these specific values are used.

Suggested change
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
assert.deepStrictEqual(node._services.length, 7);
} else {
assert.deepStrictEqual(node._services.length, 8);
}
const initialServiceCount = node._services.length;
node.createService(AddTwoInts, 'add_two_ints', () => {});
assert.deepStrictEqual(node._services.length, initialServiceCount + 1);

Copilot uses AI. Check for mistakes.


assertThrowsError(
function () {
Expand All @@ -101,7 +106,11 @@ describe('Node extra destroy methods testing', function () {
);

node.destroyService(service);
assert.deepStrictEqual(node._services.length, 6);
if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
assert.deepStrictEqual(node._services.length, 6);
} else {
assert.deepStrictEqual(node._services.length, 7);
}
});

it('destroyTimer()', function () {
Expand Down
Loading
Loading