From b743356dadb385795ff11a284928a0234cc75e27 Mon Sep 17 00:00:00 2001 From: Wayne Parrott Date: Mon, 28 Aug 2023 01:33:56 -0500 Subject: [PATCH] service introspection implementation (#914) * service introspection implementation * fixes undefined skip() function --- example/client-example.js | 71 ++-- example/service-example.js | 16 +- example/subscription-service-event-example.js | 35 ++ index.js | 4 + lib/client.js | 28 ++ lib/lifecycle.js | 5 +- lib/node_options.js | 2 +- lib/service.js | 40 ++- lib/service_introspection.js | 30 ++ package.json | 2 +- rosidl_gen/idl_generator.js | 26 +- rosidl_gen/templates/service_event.dot | 304 ++++++++++++++++++ rostsd_gen/index.js | 27 +- src/rcl_bindings.cpp | 73 ++++- test/test-service-introspection.js | 240 ++++++++++++++ test/types/main.ts | 22 +- types/base.d.ts | 1 + types/client.d.ts | 12 + types/service.d.ts | 12 + types/service_introspection.d.ts | 10 + 20 files changed, 899 insertions(+), 61 deletions(-) create mode 100644 example/subscription-service-event-example.js create mode 100644 lib/service_introspection.js create mode 100644 rosidl_gen/templates/service_event.dot create mode 100644 test/test-service-introspection.js create mode 100644 types/service_introspection.d.ts diff --git a/example/client-example.js b/example/client-example.js index 1a926e0e..9d83d5fa 100644 --- a/example/client-example.js +++ b/example/client-example.js @@ -16,35 +16,46 @@ const rclnodejs = require('../index.js'); -rclnodejs - .init() - .then(() => { - const node = rclnodejs.createNode('client_example_node'); - - const client = node.createClient( - 'example_interfaces/srv/AddTwoInts', - 'add_two_ints' +async function main() { + await rclnodejs.init(); + const node = rclnodejs.createNode('client_example_node'); + const client = node.createClient( + 'example_interfaces/srv/AddTwoInts', + 'add_two_ints' + ); + + if ( + rclnodejs.DistroUtils.getDistroId() > + rclnodejs.DistroUtils.getDistroId('humble') + ) { + // To view service events use the following command: + // ros2 topic echo "/add_two_ints/_service_event" + client.configureIntrospection( + node.getClock(), + rclnodejs.QoS.profileSystemDefault, + rclnodejs.ServiceIntrospectionStates.METADATA ); - const request = { - a: Math.floor(Math.random() * 100), - b: Math.floor(Math.random() * 100), - }; - - client.waitForService(1000).then((result) => { - if (!result) { - console.log('Error: service not available'); - rclnodejs.shutdown(); - return; - } - console.log(`Sending: ${typeof request}`, request); - client.sendRequest(request, (response) => { - console.log(`Result: ${typeof response}`, response); - rclnodejs.shutdown(); - }); - }); - - rclnodejs.spin(node); - }) - .catch((e) => { - console.log(`Error: ${e}`); + } + + const request = { + a: Math.floor(Math.random() * 100), + b: Math.floor(Math.random() * 100), + }; + + let result = await client.waitForService(1000); + if (!result) { + console.log('Error: service not available'); + rclnodejs.shutdown(); + return; + } + + console.log(`Sending: ${typeof request}`, request); + client.sendRequest(request, (response) => { + console.log(`Result: ${typeof response}`, response); + rclnodejs.shutdown(); }); + + rclnodejs.spin(node); +} + +main(); diff --git a/example/service-example.js b/example/service-example.js index 3a8e4843..2f379fae 100644 --- a/example/service-example.js +++ b/example/service-example.js @@ -21,7 +21,7 @@ rclnodejs .then(() => { let node = rclnodejs.createNode('service_example_node'); - node.createService( + let service = node.createService( 'example_interfaces/srv/AddTwoInts', 'add_two_ints', (request, response) => { @@ -33,6 +33,20 @@ rclnodejs } ); + if ( + rclnodejs.DistroUtils.getDistroId() > + rclnodejs.DistroUtils.getDistroId('humble') + ) { + console.log('Introspection configured'); + // To view service events use the following command: + // ros2 topic echo "/add_two_ints/_service_event" + service.configureIntrospection( + node.getClock(), + rclnodejs.QoS.profileSystemDefault, + rclnodejs.ServiceIntrospectionStates.CONTENTS + ); + } + rclnodejs.spin(node); }) .catch((e) => { diff --git a/example/subscription-service-event-example.js b/example/subscription-service-event-example.js new file mode 100644 index 00000000..90adff27 --- /dev/null +++ b/example/subscription-service-event-example.js @@ -0,0 +1,35 @@ +// Copyright (c) 2023 Wayne Parrott. All rights reserved. +// +// 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 rclnodejs = require('../index.js'); + +async function main() { + await rclnodejs.init(); + const node = new rclnodejs.Node('subscription_service_event_example_node'); + let count = 0; + + node.createSubscription( + 'example_interfaces/srv/AddTwoInts_Event', + '/add_two_ints/_service_event', + (event) => { + console.log(`Received event No. ${++count}: `, event); + } + ); + + node.spin(); +} + +main(); diff --git a/index.js b/index.js index 803d5b99..f32e69b1 100644 --- a/index.js +++ b/index.js @@ -52,6 +52,7 @@ const { getActionServerNamesAndTypesByNode, getActionNamesAndTypes, } = require('./lib/action/graph.js'); +const ServiceIntrospectionStates = require('./lib/service_introspection.js'); /** * Get the version of the generator that was used for the currently present interfaces. @@ -143,6 +144,9 @@ let rcl = { /** {@link ROSClock} class */ ROSClock: ROSClock, + /** {@link ServiceIntrospectionStates} */ + ServiceIntrospectionStates: ServiceIntrospectionStates, + /** {@link Time} class */ Time: Time, diff --git a/lib/client.js b/lib/client.js index f2194825..e85df39d 100644 --- a/lib/client.js +++ b/lib/client.js @@ -15,6 +15,7 @@ 'use strict'; const rclnodejs = require('bindings')('rclnodejs'); +const DistroUtils = require('./distro.js'); const Entity = require('./entity.js'); const debug = require('debug')('rclnodejs:client'); @@ -129,6 +130,33 @@ class Client extends Entity { get serviceName() { return this._serviceName; } + + /** + * Configure introspection. + * @param {Clock} clock - Clock to use for service event timestamps + * @param {QoS} qos - QoSProfile for the service event publisher + * @param {ServiceIntrospectionState} introspectionState - State to set introspection to + */ + configureIntrospection(clock, qos, introspectionState) { + if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) { + console.warn( + 'Service introspection is not supported by this versionof ROS 2' + ); + return; + } + + let type = this.typeClass.type(); + rclnodejs.configureServiceIntrospection( + this.handle, + this._nodeHandle, + clock.handle, + type.interfaceName, + type.pkgName, + qos, + introspectionState, + false + ); + } } module.exports = Client; diff --git a/lib/lifecycle.js b/lib/lifecycle.js index 3c5fb7dd..07d88808 100644 --- a/lib/lifecycle.js +++ b/lib/lifecycle.js @@ -308,6 +308,7 @@ class LifecycleNode extends Node { 'srv_get_state' ); let service = new Service( + this.handle, srvHandleObj.handle, srvHandleObj.name, loader.loadInterface('lifecycle_msgs/srv/GetState'), @@ -321,6 +322,7 @@ class LifecycleNode extends Node { 'srv_get_available_states' ); service = new Service( + this.handle, srvHandleObj.handle, srvHandleObj.name, loader.loadInterface('lifecycle_msgs/srv/GetAvailableStates'), @@ -334,6 +336,7 @@ class LifecycleNode extends Node { 'srv_get_available_transitions' ); service = new Service( + this.handle, srvHandleObj.handle, srvHandleObj.name, loader.loadInterface('lifecycle_msgs/srv/GetAvailableTransitions'), @@ -347,6 +350,7 @@ class LifecycleNode extends Node { 'srv_change_state' ); service = new Service( + this.handle, srvHandleObj.handle, srvHandleObj.name, loader.loadInterface('lifecycle_msgs/srv/ChangeState'), @@ -354,7 +358,6 @@ class LifecycleNode extends Node { (request, response) => this._onChangeState(request, response) ); this._services.push(service); - this.syncHandles(); } diff --git a/lib/node_options.js b/lib/node_options.js index 38e4dd8b..0cce2da9 100644 --- a/lib/node_options.js +++ b/lib/node_options.js @@ -88,7 +88,7 @@ class NodeOptions { /** * Get the automaticallyDeclareParametersFromOverrides. * - * @returns {boolean} - True indicates that a node shold declare declare parameters from + * @returns {boolean} - True indicates that a node should declare parameters from * it's parameter-overrides */ get automaticallyDeclareParametersFromOverrides() { diff --git a/lib/service.js b/lib/service.js index 4c7a1e16..eb12058e 100644 --- a/lib/service.js +++ b/lib/service.js @@ -15,6 +15,7 @@ 'use strict'; const rclnodejs = require('bindings')('rclnodejs'); +const DistroUtils = require('./distro.js'); const Entity = require('./entity.js'); const debug = require('debug')('rclnodejs:service'); @@ -63,8 +64,9 @@ class Response { */ class Service extends Entity { - constructor(handle, serviceName, typeClass, options, callback) { + constructor(nodeHandle, handle, serviceName, typeClass, options, callback) { super(handle, typeClass, options); + this._nodeHandle = nodeHandle; this._callback = callback; } @@ -95,7 +97,14 @@ class Service extends Entity { type.pkgName, options.qos ); - return new Service(handle, serviceName, typeClass, options, callback); + return new Service( + nodeHandle, + handle, + serviceName, + typeClass, + options, + callback + ); } /** @@ -105,6 +114,33 @@ class Service extends Entity { const fullServiceName = rclnodejs.getServiceName(this._handle); // returns /my_node/myservice return fullServiceName.split('/').pop(); } + + /** + * Configure introspection. + * @param {Clock} clock - Clock to use for service event timestamps + * @param {QoS} qos - QoSProfile for the service event publisher + * @param {ServiceIntrospectionState} introspectionState - State to set introspection to + */ + configureIntrospection(clock, qos, introspectionState) { + if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) { + console.warn( + 'Service introspection is not supported by this versionof ROS 2' + ); + return; + } + + let type = this.typeClass.type(); + rclnodejs.configureServiceIntrospection( + this.handle, + this._nodeHandle, + clock.handle, + type.interfaceName, + type.pkgName, + qos, + introspectionState, + true + ); + } } module.exports = Service; diff --git a/lib/service_introspection.js b/lib/service_introspection.js new file mode 100644 index 00000000..9a5fc766 --- /dev/null +++ b/lib/service_introspection.js @@ -0,0 +1,30 @@ +// Copyright (c) 2023 Wayne Parrott. All rights reserved. +// +// 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. + +/** + * @typedef {number} ServiceIntrospectionState + **/ + +/** + * Enum for service introspection states. + * @readonly + * @enum {ServiceIntrospectionState} + */ +const ServiceIntrospectionStates = { + OFF: 0, + METADATA: 1, + CONTENTS: 2, +}; + +module.exports = ServiceIntrospectionStates; diff --git a/package.json b/package.json index 9834354d..0c538a51 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "rimraf": "^3.0.2", "sinon": "^9.0.2", "tree-kill": "^1.2.2", - "typescript": "^4.0.3" + "typescript": "^5.0.3" }, "dependencies": { "@rclnodejs/ref-array-di": "^1.2.2", diff --git a/rosidl_gen/idl_generator.js b/rosidl_gen/idl_generator.js index 8984058a..c243b51c 100644 --- a/rosidl_gen/idl_generator.js +++ b/rosidl_gen/idl_generator.js @@ -19,6 +19,7 @@ const fse = require('fs-extra'); const path = require('path'); const parser = require('../rosidl_parser/rosidl_parser.js'); const actionMsgs = require('./action_msgs.js'); +const DistroUtils = require('../lib/distro.js'); dot.templateSettings.strip = false; dot.log = process.env.RCLNODEJS_LOG_VERBOSE || false; @@ -42,7 +43,7 @@ async function writeGeneratedCode(dir, fileName, code) { await fse.writeFile(path.join(dir, fileName), code); } -function generateServiceJSStruct(serviceInfo, dir) { +async function generateServiceJSStruct(serviceInfo, dir) { dir = path.join(dir, `${serviceInfo.pkgName}`); const fileName = serviceInfo.pkgName + @@ -51,10 +52,29 @@ function generateServiceJSStruct(serviceInfo, dir) { '__' + serviceInfo.interfaceName + '.js'; - const generatedCode = removeEmptyLines( + const generatedSrvCode = removeEmptyLines( dots.service({ serviceInfo: serviceInfo }) ); - return writeGeneratedCode(dir, fileName, generatedCode); + let result = writeGeneratedCode(dir, fileName, generatedSrvCode); + + if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) { + return result; + } + + // Otherwise, for post-Humble ROS 2 releases generate service_event msgs + await result; + const eventFileName = + serviceInfo.pkgName + + '__' + + serviceInfo.subFolder + + '__' + + serviceInfo.interfaceName + + '_Event.js'; + const generatedSrvEventCode = removeEmptyLines( + dots.service_event({ serviceInfo: serviceInfo }) + ); + + return writeGeneratedCode(dir, eventFileName, generatedSrvEventCode); } async function generateMessageJSStruct(messageInfo, dir) { diff --git a/rosidl_gen/templates/service_event.dot b/rosidl_gen/templates/service_event.dot new file mode 100644 index 00000000..933a775c --- /dev/null +++ b/rosidl_gen/templates/service_event.dot @@ -0,0 +1,304 @@ +// This file is automatically generated by Intel rclnodejs +// +// *** DO NOT EDIT directly +// +'use strict'; + +{{ + const interfaceName = it.serviceInfo.interfaceName; + const pkgName = it.serviceInfo.pkgName; + const subFolder = it.serviceInfo.subFolder; + + const baseName = it.serviceInfo.pkgName + '__' + it.serviceInfo.subFolder + '__' + it.serviceInfo.interfaceName; +}} + +const ref = require('@rclnodejs/ref-napi'); +const StructType = require('@rclnodejs/ref-struct-di')(ref); +const ArrayType = require('@rclnodejs/ref-array-di')(ref); +const primitiveTypes = require('../../rosidl_gen/primitive_types.js'); +const deallocator = require('../../rosidl_gen/deallocator.js'); +const translator = require('../../rosidl_gen/message_translator.js'); +const ServiceEventInfoWrapper = require('../service_msgs/service_msgs__msg__ServiceEventInfo.js'); +const {{=interfaceName}}_RequestWrapper = require('./{{=pkgName}}__{{=subFolder}}__{{=interfaceName}}_Request.js'); +const {{=interfaceName}}_ResponseWrapper = require('./{{=pkgName}}__{{=subFolder}}__{{=interfaceName}}_Response.js'); +const {{=interfaceName}}_EventRefStruct = StructType({ + info: ServiceEventInfoWrapper.refObjectType, + request: {{=interfaceName}}_RequestWrapper.refObjectArrayType, + response: {{=interfaceName}}_ResponseWrapper.refObjectArrayType, +}); +const {{=interfaceName}}_EventRefArray = ArrayType({{=interfaceName}}_EventRefStruct); +const {{=interfaceName}}_EventRefStructArray = StructType({ + data: {{=interfaceName}}_EventRefArray, + size: ref.types.size_t, + capacity: ref.types.size_t +}); +// Define the wrapper class. +class {{=interfaceName}}_EventWrapper { + constructor(other) { + this._wrapperFields = {}; + if (typeof other === 'object' && other._refObject) { + this._refObject = new {{=interfaceName}}_EventRefStruct(other._refObject.toObject()); + this._wrapperFields.info = new ServiceEventInfoWrapper(other._wrapperFields.info); + this._wrapperFields.request = {{=interfaceName}}_RequestWrapper.createArray(); + this._wrapperFields.request.copy(other._wrapperFields.request); + this._wrapperFields.response = {{=interfaceName}}_ResponseWrapper.createArray(); + this._wrapperFields.response.copy(other._wrapperFields.response); + } else if (typeof other !== 'undefined') { + this._initMembers(); + translator.constructFromPlanObject(this, other); + } else { + this._initMembers(); + } + this.freeze(); + } + _initMembers() { + this._refObject = new {{=interfaceName}}_EventRefStruct(); + this._wrapperFields.info = new ServiceEventInfoWrapper(); + this._wrapperFields.request = {{=interfaceName}}_RequestWrapper.createArray(); + this._wrapperFields.response = {{=interfaceName}}_ResponseWrapper.createArray(); + } + static createFromRefObject(refObject) { + let self = new {{=interfaceName}}_EventWrapper(); + self.copyRefObject(refObject); + return self; + } + static createArray() { + return new {{=interfaceName}}_EventArrayWrapper; + } + static get ArrayType() { + return {{=interfaceName}}_EventArrayWrapper; + } + static get refObjectArrayType() { + return {{=interfaceName}}_EventRefStructArray + } + static get refObjectType() { + return {{=interfaceName}}_EventRefStruct; + } + toRawROS() { + this.freeze(true); + return this._refObject.ref(); + } + freeze(own = false, checkConsistency = false) { + if (checkConsistency) { + } + this._wrapperFields.info.freeze(own, checkConsistency); + this._refObject.info = this._wrapperFields.info.refObject; + this._wrapperFields.request.freeze(own, checkConsistency); + this._refObject.request = this._wrapperFields.request.refObject; + this._wrapperFields.response.freeze(own, checkConsistency); + this._refObject.response = this._wrapperFields.response.refObject; + } + serialize() { + this.freeze(false, true); + return this._refObject.ref(); + } + deserialize(refObject) { + this._wrapperFields.info.copyRefObject(refObject.info); + this._wrapperFields.request.copyRefObject(refObject.request); + this._wrapperFields.response.copyRefObject(refObject.response); + } + toPlainObject(enableTypedArray) { + return translator.toPlainObject(this, enableTypedArray); + } + static freeStruct(refObject) { + ServiceEventInfoWrapper.freeStruct(refObject.info); + if (refObject.request.size != 0) { + {{=interfaceName}}_RequestWrapper.ArrayType.freeArray(refObject.request); + if ({{=interfaceName}}_RequestWrapper.ArrayType.useTypedArray) { + // Do nothing, the v8 will take the ownership of the ArrayBuffer used by the typed array. + } else { + deallocator.freeStructMember(refObject.request, {{=interfaceName}}_RequestWrapper.refObjectArrayType, 'data'); + } + } + if (refObject.response.size != 0) { + {{=interfaceName}}_ResponseWrapper.ArrayType.freeArray(refObject.response); + if ({{=interfaceName}}_ResponseWrapper.ArrayType.useTypedArray) { + // Do nothing, the v8 will take the ownership of the ArrayBuffer used by the typed array. + } else { + deallocator.freeStructMember(refObject.response, {{=interfaceName}}_ResponseWrapper.refObjectArrayType, 'data'); + } + } + } + static destoryRawROS(msg) { + {{=interfaceName}}_EventWrapper.freeStruct(msg.refObject); + } + static type() { + return {pkgName: '{{=pkgName}}', subFolder: '{{=subFolder}}', interfaceName: '{{=interfaceName}}_Event'}; + } + static isPrimitive() { + return false; + } + static get isROSArray() { + return false; + } + get refObject() { + return this._refObject; + } + get info() { + return this._wrapperFields.info; + } + set info(value) { + if (value instanceof ServiceEventInfoWrapper) { + this._wrapperFields.info.copy(value); + } else { + this._wrapperFields.info.copy(new ServiceEventInfoWrapper(value)); + } + } + get request() { + return this._wrapperFields.request; + } + set request(value) { + if (value.length > 1) { + throw new RangeError('The length of array request must be <= 1.'); + } + this._wrapperFields.request.fill(value); + } + get response() { + return this._wrapperFields.response; + } + set response(value) { + if (value.length > 1) { + throw new RangeError('The length of array response must be <= 1.'); + } + this._wrapperFields.response.fill(value); + } + copyRefObject(refObject) { + this._refObject = new {{=interfaceName}}_EventRefStruct(refObject.toObject()); + this._wrapperFields.info.copyRefObject(this._refObject.info); + this._wrapperFields.request.copyRefObject(this._refObject.request); + this._wrapperFields.response.copyRefObject(this._refObject.response); + } + copy(other) { + this._refObject = new {{=interfaceName}}_EventRefStruct(other._refObject.toObject()); + this._wrapperFields.info.copy(other._wrapperFields.info); + this._wrapperFields.request.copy(other._wrapperFields.request); + this._wrapperFields.response.copy(other._wrapperFields.response); + } + static get classType() { + return {{=interfaceName}}_EventWrapper; + } + static get ROSMessageDef() { + return {"constants":[],"fields":[{"name":"info","type":{"isArray":false,"arraySize":null,"isUpperBound":false,"isDynamicArray":false,"isFixedSizeArray":false,"pkgName":"service_msgs","type":"ServiceEventInfo","stringUpperBound":null,"isPrimitiveType":false},"default_value":null},{"name":"request","type":{"isArray":true,"arraySize":1,"isUpperBound":true,"isDynamicArray":true,"isFixedSizeArray":false,"pkgName":"{{=pkgName}}","type":"{{=interfaceName}}_Request","stringUpperBound":null,"isPrimitiveType":false},"default_value":null},{"name":"response","type":{"isArray":true,"arraySize":1,"isUpperBound":true,"isDynamicArray":true,"isFixedSizeArray":false,"pkgName":"{{=pkgName}}","type":"{{=interfaceName}}_Response","stringUpperBound":null,"isPrimitiveType":false},"default_value":null}],"baseType":{"pkgName":"{{=pkgName}}","type":"{{=interfaceName}}_Event","stringUpperBound":null,"isPrimitiveType":false},"msgName":"{{=interfaceName}}_Event"}; + } + hasMember(name) { + let memberNames = ["info","request","response"]; + return memberNames.indexOf(name) !== -1; + } +} +// Define the wrapper of array class. +class {{=interfaceName}}_EventArrayWrapper { + constructor(size = 0) { + this._resize(size); + } + toRawROS() { + return this._refObject.ref(); + } + fill(values) { + const length = values.length; + this._resize(length); + values.forEach((value, index) => { + if (value instanceof {{=interfaceName}}_EventWrapper) { + this._wrappers[index].copy(value); + } else { + this._wrappers[index] = new {{=interfaceName}}_EventWrapper(value); + } + }); + } + // Put all data currently stored in `this._wrappers` into `this._refObject` + freeze(own) { + this._wrappers.forEach((wrapper, index) => { + wrapper.freeze(own); + this._refArray[index] = wrapper.refObject; + }); + this._refObject.size = this._wrappers.length; + this._refObject.capacity = this._wrappers.length; + if (this._refObject.capacity === 0) { + this._refObject.data = null + } else { + this._refObject.data = this._refArray.buffer; + } + } + get refObject() { + return this._refObject; + } + get data() { + return this._wrappers; + } + get size() { + return this._wrappers.length; + } + set size(value) { + if (typeof value != 'number') { + throw new TypeError('Invalid argument: should provide a number to {{=interfaceName}}_EventArrayWrapper.size setter'); + return; + } + return this._resize(value); + } + get capacity() { + return this._wrappers.length; + } + set capacity(value) { + if (typeof value != 'number') { + throw new TypeError('Invalid argument: should provide a number to {{=interfaceName}}_EventArrayWrapper.capacity setter'); + } + return this._resize(value); + } + get refObject() { + return this._refObject; + } + _resize(size) { + if (size < 0) { + throw new RangeError('Invalid argument: should provide a positive number'); + return; + } + this._refArray = new {{=interfaceName}}_EventRefArray(size); + this._refObject = new {{=interfaceName}}_EventRefStructArray(); + this._refObject.size = size; + this._refObject.capacity = size; + this._wrappers = new Array(); + for (let i = 0; i < size; i++) { + this._wrappers.push(new {{=interfaceName}}_EventWrapper()); + } + } + // Copy all data from `this._refObject` into `this._wrappers` + copyRefObject(refObject) { + this._refObject = refObject; + let refObjectArray = this._refObject.data; + refObjectArray.length = this._refObject.size; + this._resize(this._refObject.size); + for (let index = 0; index < this._refObject.size; index++) { + this._wrappers[index].copyRefObject(refObjectArray[index]); + } + } + copy(other) { + if (! (other instanceof {{=interfaceName}}_EventArrayWrapper)) { + throw new TypeError('Invalid argument: should provide "{{=interfaceName}}_EventArrayWrapper".'); + } + this._resize(other.size); + // Array deep copy + other._wrappers.forEach((wrapper, index) => { + this._wrappers[index].copy(wrapper); + }); + } + static freeArray(refObject) { + let refObjectArray = refObject.data; + refObjectArray.length = refObject.size; + for (let index = 0; index < refObject.size; index++) { + {{=interfaceName}}_EventWrapper.freeStruct(refObjectArray[index]); + } + } + static get elementType() { + return {{=interfaceName}}_EventWrapper; + } + static get isROSArray() { + return true; + } + static get useTypedArray() { + return false; + } + get classType() { + return {{=interfaceName}}_EventArrayWrapper; + } +} +module.exports = {{=interfaceName}}_EventWrapper; + diff --git a/rostsd_gen/index.js b/rostsd_gen/index.js index 97365ade..dbb9b647 100644 --- a/rostsd_gen/index.js +++ b/rostsd_gen/index.js @@ -38,9 +38,9 @@ async function generateAll() { const generatedPath = path.join(__dirname, '../generated/'); const pkgInfos = getPkgInfos(generatedPath); - // write message.d.ts file - const messagesFilePath = path.join(__dirname, '../types/interfaces.d.ts'); - const fd = fs.openSync(messagesFilePath, 'w'); + // write interfaces.d.ts file + const interfacesFilePath = path.join(__dirname, '../types/interfaces.d.ts'); + const fd = fs.openSync(interfacesFilePath, 'w'); savePkgInfoAsTSD(pkgInfos, fd); await wait(500); // hack to avoid random segfault fs.closeSync(fd); @@ -235,7 +235,9 @@ function saveMsgAsTSD(rosMsgInterface, fd) { fd, ` export interface ${rosMsgInterface.type().interfaceName} {\n` ); - const useSamePkg = isInternalActionMsgInterface(rosMsgInterface); + const useSamePkg = + isInternalActionMsgInterface(rosMsgInterface) || + isInternalServiceEventMsgInterface(rosMsgInterface); saveMsgFieldsAsTSD(rosMsgInterface, fd, 8, ';', '', useSamePkg); fs.writeSync(fd, ' }\n'); } @@ -270,7 +272,6 @@ function saveMsgFieldsAsTSD( useSamePackageSubFolder && field.type.pkgName === type.pkgName ? type.subFolder : 'msg'; - let fieldType = fieldType2JSName(field, subFolder); let tp = field.type.isPrimitiveType ? '' : typePrefix; if (typePrefix === 'rclnodejs.') { @@ -346,13 +347,6 @@ function isMsgInterface(rosInterface) { return rosInterface.hasOwnProperty('ROSMessageDef'); } -function isServiceMsgInterface(rosMsgInterface) { - if (!isMsgInterface(rosMsgInterface)) return false; - - let name = rosMsgInterface.type().interfaceName; - return name.endsWith('_Request') || name.endsWith('_Response'); -} - function isInternalActionMsgInterface(rosMsgInterface) { let name = rosMsgInterface.type().interfaceName; return ( @@ -364,6 +358,15 @@ function isInternalActionMsgInterface(rosMsgInterface) { ); } +function isInternalServiceEventMsgInterface(rosMsgInterface) { + let name = rosMsgInterface.type().interfaceName; + let subFolder = rosMsgInterface.type().subFolder; + return ( + (subFolder == 'srv' || subFolder == 'action') + && name.endsWith('_Event') + ); +} + function isSrvInterface(rosInterface) { return ( rosInterface.hasOwnProperty('Request') && diff --git a/src/rcl_bindings.cpp b/src/rcl_bindings.cpp index 905d660f..71c42375 100644 --- a/src/rcl_bindings.cpp +++ b/src/rcl_bindings.cpp @@ -36,6 +36,10 @@ #include #endif +#if ROS_VERSION > 2205 +#include +#endif + #include #include #include @@ -1120,6 +1124,68 @@ NAN_METHOD(SendResponse) { RCL_RET_OK, rcl_get_error_string().str); } +#if ROS_VERSION > 2205 // 2205 == Humble +NAN_METHOD(ConfigureServiceIntrospection) { + v8::Local currentContent = Nan::GetCurrentContext(); + + RclHandle* node_handle = RclHandle::Unwrap( + Nan::To(info[1]).ToLocalChecked()); + rcl_node_t* node = reinterpret_cast(node_handle->ptr()); + + rcl_clock_t* clock = reinterpret_cast( + RclHandle::Unwrap( + Nan::To(info[2]).ToLocalChecked()) + ->ptr()); + + std::string interface_name( + *Nan::Utf8String(info[3]->ToString(currentContent).ToLocalChecked())); + std::string package_name( + *Nan::Utf8String(info[4]->ToString(currentContent).ToLocalChecked())); + const rosidl_service_type_support_t* ts = + GetServiceTypeSupport(package_name, interface_name); + + if (ts) { + rcl_publisher_options_t publisher_ops = rcl_publisher_get_default_options(); + auto qos_profile = GetQoSProfile(info[5]); + if (qos_profile) { + publisher_ops.qos = *qos_profile; + } + + rcl_service_introspection_state_t state = + static_cast( + Nan::To(info[6]).ToChecked()); + + bool configureForService = Nan::To(info[7]).FromJust(); + + if (configureForService) { + RclHandle* service_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_service_t* service = + reinterpret_cast(service_handle->ptr()); + + THROW_ERROR_IF_NOT_EQUAL( + rcl_service_configure_service_introspection(service, node, clock, ts, + publisher_ops, state), + RCL_RET_OK, rcl_get_error_string().str); + + } else { + RclHandle* client_handle = RclHandle::Unwrap( + Nan::To(info[0]).ToLocalChecked()); + rcl_client_t* client = + reinterpret_cast(client_handle->ptr()); + + THROW_ERROR_IF_NOT_EQUAL( + rcl_client_configure_service_introspection(client, node, clock, ts, + publisher_ops, state), + RCL_RET_OK, rcl_get_error_string().str); + } + + } else { + Nan::ThrowError(GetErrorMessageAndClear().c_str()); + } +} +#endif + NAN_METHOD(ValidateFullTopicName) { v8::Local currentContent = Nan::GetCurrentContext(); int validation_result; @@ -2011,6 +2077,11 @@ std::vector binding_methods = { {"serviceServerIsAvailable", ServiceServerIsAvailable}, {"publishRawMessage", PublishRawMessage}, {"rclTakeRaw", RclTakeRaw}, - {"", nullptr}}; + {"", nullptr} +#if ROS_VERSION > 2205 // 2205 == Humble + , + {"configureServiceIntrospection", ConfigureServiceIntrospection} +#endif +}; } // namespace rclnodejs diff --git a/test/test-service-introspection.js b/test/test-service-introspection.js new file mode 100644 index 00000000..e75b63e2 --- /dev/null +++ b/test/test-service-introspection.js @@ -0,0 +1,240 @@ +// Copyright (c) 2023 Wayne Parrott. All rights reserved. +// +// 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 childProcess = require('child_process'); +const assert = require('assert'); +const rclnodejs = require('../index.js'); +const DistroUtils = rclnodejs.DistroUtils; +const YAML = require('yaml'); + +const util = require('node:util'); +const exec = util.promisify(require('node:child_process').exec); + +const ServiceIntrospectionStates = rclnodejs.ServiceIntrospectionStates; +const QOS = rclnodejs.QoS.profileSystemDefault; + +const DELAY = 1000; // ms + +// ros2 topic echo /add_two_ints/_service_event + +function isServiceIntrospectionSupported() { + return DistroUtils.getDistroId() > DistroUtils.getDistroId('humble'); +} + +function runClient(client, request, delay = DELAY) { + client.sendRequest(request, (response) => { + // do nothing + }); + + return new Promise((resolve) => { + setTimeout(resolve, delay); + }); +} + +describe('service introspection', function () { + this.timeout(30 * 1000); + + let node; + let service; + let client; + let request; + let serviceEventSubscriber; + let eventQueue; + + before( function() { + if (!isServiceIntrospectionSupported()) { + this.skip(); + } + }); + + beforeEach(async function () { + await rclnodejs.init(); + + this.node = new rclnodejs.Node('service_example_node'); + + this.service = this.node.createService( + 'example_interfaces/srv/AddTwoInts', + 'add_two_ints', + (request, response) => { + let result = response.template; + result.sum = request.a + request.b; + response.send(result); + } + ); + + this.client = this.node.createClient( + 'example_interfaces/srv/AddTwoInts', + 'add_two_ints' + ); + + if (!(await this.client.waitForService(1000))) { + rclnodejs.shutdown(); + throw new Error('client unable to access service'); + } + + this.request = { + a: Math.floor(Math.random() * 100), + b: Math.floor(Math.random() * 100), + }; + + this.eventQueue = []; + let eventQueueRef = this.eventQueue; + this.serviceEventSubscriber = this.node.createSubscription( + 'example_interfaces/srv/AddTwoInts_Event', + '/add_two_ints/_service_event', + function (event) { + eventQueueRef.push(event); + } + ); + + this.node.spin(); + }); + + afterEach(function () { + rclnodejs.shutdown(); + }); + + it('client and service introspection: CONTENT', async function () { + this.service.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.CONTENTS + ); + + this.client.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.CONTENTS + ); + + await runClient(this.client, this.request); + + assert.strictEqual(this.eventQueue.length, 4); + for (let i = 0; i < 4; i++) { + assert.strictEqual(this.eventQueue[i].info.event_type, i); + if (i < 2) { + assert.strictEqual(this.eventQueue[i].request.length, 1); + assert.strictEqual(this.eventQueue[i].response.length, 0); + } else { + assert.strictEqual(this.eventQueue[i].request.length, 0); + assert.strictEqual(this.eventQueue[i].response.length, 1); + } + } + }); + + it('service-only introspection: METADATA', async function () { + this.service.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.METADATA + ); + + await runClient(this.client, this.request); + + assert.strictEqual(this.eventQueue.length, 2); + + assert.strictEqual(this.eventQueue[0].info.event_type, 1); + assert.strictEqual(this.eventQueue[0].request.length, 0); + assert.strictEqual(this.eventQueue[0].response.length, 0); + + assert.strictEqual(this.eventQueue[1].info.event_type, 2); + assert.strictEqual(this.eventQueue[1].request.length, 0); + assert.strictEqual(this.eventQueue[1].response.length, 0); + }); + + it('client-only introspection: METADATA', async function () { + this.client.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.METADATA + ); + + await runClient(this.client, this.request); + + assert.strictEqual(this.eventQueue.length, 2); + + assert.strictEqual(this.eventQueue[0].info.event_type, 0); + assert.strictEqual(this.eventQueue[0].request.length, 0); + assert.strictEqual(this.eventQueue[0].response.length, 0); + + assert.strictEqual(this.eventQueue[1].info.event_type, 3); + assert.strictEqual(this.eventQueue[1].request.length, 0); + assert.strictEqual(this.eventQueue[1].response.length, 0); + }); + + it('client and service unconfigured introspection', async function () { + await runClient(this.client, this.request); + assert.strictEqual(this.eventQueue.length, 0); + }); + + it('client introspection: OFF, service introspection: OFF', async function () { + this.service.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.OFF + ); + + this.client.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.OFF + ); + + await runClient(this.client, this.request); + + assert.strictEqual(this.eventQueue.length, 0); + }); + + it('client introspection: OFF, service introspection: CONTENTS', async function () { + this.service.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.CONTENTS + ); + + this.client.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.OFF + ); + + await runClient(this.client, this.request); + + assert.strictEqual(this.eventQueue.length, 2); + assert.strictEqual(this.eventQueue[0].info.event_type, 1); + assert.strictEqual(this.eventQueue[1].info.event_type, 2); + }); + + it('client introspection: CONTENTS, service introspection: OFF', async function () { + this.service.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.OFF + ); + + this.client.configureIntrospection( + this.node.getClock(), + QOS, + ServiceIntrospectionStates.CONTENTS + ); + + await runClient(this.client, this.request); + + assert.strictEqual(this.eventQueue.length, 2); + assert.strictEqual(this.eventQueue[0].info.event_type, 0); + assert.strictEqual(this.eventQueue[1].info.event_type, 3); + }); +}); diff --git a/test/types/main.ts b/test/types/main.ts index 26b449df..da9f32b9 100644 --- a/test/types/main.ts +++ b/test/types/main.ts @@ -250,26 +250,24 @@ subscription.hasContentFilter(); // ---- Service ---- // $ExpectType AddTwoIntsConstructor -let service = node.createService( +const service = node.createService( 'example_interfaces/srv/AddTwoInts', 'add_two_ints', (request, response) => {} ); -// $ExpectType AddTwoIntsConstructor -service = node.createService( - 'example_interfaces/srv/AddTwoInts', - 'add_two_ints', - {}, - (request, response) => {} -); - // $ExpectType string service.serviceName; // $ExpectType object service.options; +service.configureIntrospection( + node.getClock(), + rclnodejs.Node.getDefaultOptions() as rclnodejs.QoS, + rclnodejs.ServiceIntrospectionStates.CONTENTS +); + // $ExpectType boolean service.isDestroyed(); @@ -289,6 +287,12 @@ client.isServiceServerAvailable(); // $ExpectType Promise client.waitForService(); +client.configureIntrospection( + node.getClock(), + rclnodejs.Node.getDefaultOptions() as rclnodejs.QoS, + rclnodejs.ServiceIntrospectionStates.CONTENTS +); + // $ExpectType boolean client.isDestroyed(); diff --git a/types/base.d.ts b/types/base.d.ts index 50426dcc..1bf78ef6 100644 --- a/types/base.d.ts +++ b/types/base.d.ts @@ -21,6 +21,7 @@ /// /// /// +/// /// /// /// diff --git a/types/client.d.ts b/types/client.d.ts index 7d484457..8c9c64e0 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -40,6 +40,18 @@ declare module 'rclnodejs' { * Name of the service to which requests are made. */ readonly serviceName: string; + + /** + * Configure introspection. + * @param clock - Clock to use for service event timestamps + * @param QoSProfile - QOS profile for the service event publisher + * @param introspectionState - The state to set introspection to + */ + configureIntrospection( + clock: Clock, + serviceEventPubQOS: QoS, + introspectionState: ServiceIntrospectionStates + ): void; } namespace Client { diff --git a/types/service.d.ts b/types/service.d.ts index 92fd96d9..f766b05b 100644 --- a/types/service.d.ts +++ b/types/service.d.ts @@ -76,5 +76,17 @@ declare module 'rclnodejs' { * Name of the service. */ readonly serviceName: string; + + /** + * Configure introspection. + * @param clock - Clock to use for service event timestamps + * @param QoSProfile - QOS profile for the service event publisher + * @param introspectionState - The state to set introspection to + */ + configureIntrospection( + clock: Clock, + serviceEventPubQOS: QoS, + introspectionState: ServiceIntrospectionStates + ): void; } } diff --git a/types/service_introspection.d.ts b/types/service_introspection.d.ts new file mode 100644 index 00000000..7f4970ae --- /dev/null +++ b/types/service_introspection.d.ts @@ -0,0 +1,10 @@ +declare module 'rclnodejs' { + /** + * State identifiers for service introspection support. + */ + export const enum ServiceIntrospectionStates { + OFF = 0, + METADATA = 1, + CONTENTS = 2, + } +}