diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 85e078af..87b494d6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,6 +1,6 @@ # rclnodejs contributors (sorted alphabetically) -- **[Alaa El Jawad](https://github.com/ejalaa12), [Ian McElroy](https://github.com/imcelroy)** +- **[Alaa El Jawad](https://github.com/ejalaa12)** - Fix compatibility with ROS2 parameters array types - Unit tests for all parameter types @@ -20,6 +20,13 @@ - Benchmark test script +- **[Ian McElroy](https://github.com/imcelroy)** + + - Add descriptor namespace for all interfaces, rostsd_gen improvements + - Fix compatibility with ROS2 parameters array types + - Unit tests for all parameter types + - Handle concurrent ROS2 client calls, with unit tests + - **[Kenny Yuan](https://github.com/kenny-y)** - Message features: JS generation, typed arrays, plain JS object, compound msgs, many others... diff --git a/rostsd_gen/index.js b/rostsd_gen/index.js index 9cb2fc65..957252db 100644 --- a/rostsd_gen/index.js +++ b/rostsd_gen/index.js @@ -31,6 +31,8 @@ const fs = require('fs'); const loader = require('../lib/interface_loader.js'); const pkgFilters = require('../rosidl_gen/filter.js'); +const descriptorInterfaceNamespace = 'descriptor'; + async function generateAll() { // load pkg and interface info (msgs and srvs) const generatedPath = path.join(__dirname, '../generated/'); @@ -119,47 +121,30 @@ function savePkgInfoAsTSD(pkgInfos, fd) { for (const subfolder of pkgInfo.subfolders.keys()) { fs.writeSync(fd, ` namespace ${subfolder} {\n`); - for (const rosInterface of pkgInfo.subfolders.get(subfolder)) { - const type = rosInterface.type(); - const fullInterfaceName = `${type.pkgName}/${type.subFolder}/${type.interfaceName}`; - const fullInterfacePath = `${type.pkgName}.${type.subFolder}.${type.interfaceName}`; - const fullInterfaceConstructor = fullInterfacePath + 'Constructor'; - - if (isMsgInterface(rosInterface)) { - // create message interface - saveMsgAsTSD(rosInterface, fd); - saveMsgConstructorAsTSD(rosInterface, fd); - messagesMap[fullInterfaceName] = fullInterfacePath; - } else if (isSrvInterface(rosInterface)) { - if ( - !isValidService(rosInterface, pkgInfo.subfolders.get(subfolder)) - ) { - let type = rosInterface.type(); - console.log( - `Incomplete service: ${type.pkgName}.${type.subFolder}.${type.interfaceName}.` - ); - continue; - } - - // create service interface - saveSrvAsTSD(rosInterface, fd); - if (!isInternalActionSrvInterface(rosInterface)) { - servicesMap[fullInterfaceName] = fullInterfaceConstructor; - } - } else if (isActionInterface(rosInterface)) { - if (!isValidAction(rosInterface, pkgInfo.subfolders.get(subfolder))) { - let type = rosInterface.type(); - console.log( - `Incomplete action: ${type.pkgName}.${type.subFolder}.${type.interfaceName}.` - ); - continue; - } - - // create action interface - saveActionAsTSD(rosInterface, fd); - actionsMap[fullInterfaceName] = fullInterfaceConstructor; - } - } + // generate real msg/srv/action interfaces + generateRosMsgInterfaces( + pkgInfo, + subfolder, + messagesMap, + servicesMap, + actionsMap, + fd + ); + + // generate descriptor msg/srv/action interfaces + fs.writeSync(fd, ` namespace ${descriptorInterfaceNamespace} {\n`); + const willGenerateDescriptorInterface = true; + generateRosMsgInterfaces( + pkgInfo, + subfolder, + messagesMap, + servicesMap, + actionsMap, + fd, + willGenerateDescriptorInterface + ); + // close namespace descriptor declare + fs.writeSync(fd, ' }\n'); // close namespace declare fs.writeSync(fd, ' }\n'); @@ -238,16 +223,96 @@ function savePkgInfoAsTSD(pkgInfos, fd) { fs.writeSync(fd, '}\n'); } -function saveMsgAsTSD(rosMsgInterface, fd) { - fs.writeSync( - fd, - ` export interface ${rosMsgInterface.type().interfaceName} {\n` +function generateRosMsgInterfaces( + pkgInfo, + subfolder, + messagesMap, + servicesMap, + actionsMap, + fd, + willGenerateDescriptorInterface = false +) { + const descriptorNamespaceName = willGenerateDescriptorInterface + ? `${descriptorInterfaceNamespace}/` + : ''; + const descriptorNamespacePath = willGenerateDescriptorInterface + ? `${descriptorInterfaceNamespace}.` + : ''; + for (const rosInterface of pkgInfo.subfolders.get(subfolder)) { + const type = rosInterface.type(); + const fullInterfaceName = `${type.pkgName}/${type.subFolder}/${descriptorNamespaceName}${type.interfaceName}`; + const fullInterfacePath = `${type.pkgName}.${type.subFolder}.${descriptorNamespacePath}${type.interfaceName}`; + const fullInterfaceConstructor = fullInterfacePath + 'Constructor'; + + const indentStartLevel = willGenerateDescriptorInterface ? 4 : 3; + if (isMsgInterface(rosInterface)) { + // create message interface + saveMsgAsTSD( + rosInterface, + fd, + indentStartLevel, + willGenerateDescriptorInterface + ); + saveMsgConstructorAsTSD(rosInterface, fd, indentStartLevel); + messagesMap[fullInterfaceName] = fullInterfacePath; + } else if (isSrvInterface(rosInterface)) { + if (!isValidService(rosInterface, pkgInfo.subfolders.get(subfolder))) { + let type = rosInterface.type(); + console.log( + `Incomplete service: ${type.pkgName}.${type.subFolder}.${type.interfaceName}.` + ); + continue; + } + + // create service interface + saveSrvAsTSD(rosInterface, fd, indentStartLevel); + if (!isInternalActionSrvInterface(rosInterface)) { + servicesMap[fullInterfaceName] = fullInterfaceConstructor; + } + } else if (isActionInterface(rosInterface)) { + if (!isValidAction(rosInterface, pkgInfo.subfolders.get(subfolder))) { + let type = rosInterface.type(); + console.log( + `Incomplete action: ${type.pkgName}.${type.subFolder}.${type.interfaceName}.` + ); + continue; + } + + // create action interface + saveActionAsTSD(rosInterface, fd, indentStartLevel); + actionsMap[fullInterfaceName] = fullInterfaceConstructor; + } + } +} + +function saveMsgAsTSD( + rosMsgInterface, + fd, + indentLevel = 3, + willGenerateDescriptorInterface = false +) { + const outerIndentSpacing = getIndentSpacing(indentLevel); + const tmpl = indentString( + `export interface ${rosMsgInterface.type().interfaceName} {\n`, + outerIndentSpacing ); + fs.writeSync(fd, tmpl); const useSamePkg = isInternalActionMsgInterface(rosMsgInterface) || isInternalServiceEventMsgInterface(rosMsgInterface); - saveMsgFieldsAsTSD(rosMsgInterface, fd, 8, ';', '', useSamePkg); - fs.writeSync(fd, ' }\n'); + const innerIndentLevel = indentLevel + 1; + const innerIndentSpacing = getIndentSpacing(innerIndentLevel); + saveMsgFieldsAsTSD( + rosMsgInterface, + fd, + innerIndentSpacing, + ';', + '', + useSamePkg, + willGenerateDescriptorInterface + ); + const tmplEnd = indentString('}\n', outerIndentSpacing); + fs.writeSync(fd, tmplEnd); } /** @@ -261,6 +326,7 @@ function saveMsgAsTSD(rosMsgInterface, fd) { * @param {string} typePrefix The prefix to put before the type name for * non-primitive types * @param {boolean} useSamePackageSubFolder Indicates if the sub folder name should be taken from the message + * @param {boolean} willGenerateDescriptorInterface Indicates if descriptor interface is being generated * when the field type comes from the same package. This is needed for action interfaces. Defaults to false. * @returns {undefined} */ @@ -270,7 +336,8 @@ function saveMsgFieldsAsTSD( indent = 0, lineEnd = ',', typePrefix = '', - useSamePackageSubFolder = false + useSamePackageSubFolder = false, + willGenerateDescriptorInterface = false ) { let type = rosMsgInterface.type(); let fields = rosMsgInterface.ROSMessageDef.fields; @@ -280,49 +347,62 @@ function saveMsgFieldsAsTSD( useSamePackageSubFolder && field.type.pkgName === type.pkgName ? type.subFolder : 'msg'; - let fieldType = fieldType2JSName(field, subFolder); + let fieldType = fieldType2JSName( + field, + subFolder, + willGenerateDescriptorInterface + ); let tp = field.type.isPrimitiveType ? '' : typePrefix; if (typePrefix === 'rclnodejs.') { fieldType = 'any'; tp = ''; } - const tmpl = indentString(`${field.name}: ${tp}${fieldType}`, indent); - fs.writeSync(fd, tmpl); + let arrayString = ''; if (field.type.isArray) { - fs.writeSync(fd, '[]'); + arrayString = '[]'; + + if (field.type.isFixedSizeArray && willGenerateDescriptorInterface) { + arrayString = `[${field.type.arraySize}]`; + } - if (fieldType === 'number') { + if (fieldType === 'number' && !willGenerateDescriptorInterface) { // for number[] include alternate typed-array types, e.g., number[] | uint8[] let jsTypedArrayName = fieldTypeArray2JSTypedArrayName(field.type.type); if (jsTypedArrayName) { - fs.writeSync(fd, ` | ${jsTypedArrayName}`); + arrayString += ` | ${jsTypedArrayName}`; } } } + const fieldString = willGenerateDescriptorInterface + ? `${field.name}: '${tp}${fieldType}${arrayString}'` + : `${field.name}: ${tp}${fieldType}${arrayString}`; + const tmpl = indentString(fieldString, indent); + fs.writeSync(fd, tmpl); fs.writeSync(fd, lineEnd); fs.writeSync(fd, '\n'); } } -function saveMsgConstructorAsTSD(rosMsgInterface, fd) { +function saveMsgConstructorAsTSD(rosMsgInterface, fd, indentLevel = 3) { const type = rosMsgInterface.type(); const msgName = type.interfaceName; - - fs.writeSync(fd, ` export interface ${msgName}Constructor {\n`); + let interfaceTmpl = [`export interface ${msgName}Constructor {`]; for (const constant of rosMsgInterface.ROSMessageDef.constants) { const constantType = primitiveType2JSName(constant.type); - fs.writeSync(fd, ` readonly ${constant.name}: ${constantType};\n`); + interfaceTmpl.push(` readonly ${constant.name}: ${constantType};`); } - - fs.writeSync(fd, ` new(other?: ${msgName}): ${msgName};\n`); - fs.writeSync(fd, ' }\n'); + interfaceTmpl.push(` new(other?: ${msgName}): ${msgName};`); + interfaceTmpl.push('}'); + interfaceTmpl.push(''); + const indentSpacing = getIndentSpacing(indentLevel); + fs.writeSync(fd, indentLines(interfaceTmpl, indentSpacing).join('\n')); } -function saveSrvAsTSD(rosSrvInterface, fd) { +function saveSrvAsTSD(rosSrvInterface, fd, indentLevel = 3) { const serviceName = rosSrvInterface.type().interfaceName; const interfaceTemplate = [ @@ -332,11 +412,11 @@ function saveSrvAsTSD(rosSrvInterface, fd) { '}', '', ]; - - fs.writeSync(fd, indentLines(interfaceTemplate, 6).join('\n')); + const indentSpacing = getIndentSpacing(indentLevel); + fs.writeSync(fd, indentLines(interfaceTemplate, indentSpacing).join('\n')); } -function saveActionAsTSD(rosActionInterface, fd) { +function saveActionAsTSD(rosActionInterface, fd, indentLevel = 3) { const actionName = rosActionInterface.type().interfaceName; const interfaceTemplate = [ @@ -347,8 +427,19 @@ function saveActionAsTSD(rosActionInterface, fd) { '}', '', ]; + const indentSpacing = getIndentSpacing(indentLevel); + fs.writeSync(fd, indentLines(interfaceTemplate, indentSpacing).join('\n')); +} - fs.writeSync(fd, indentLines(interfaceTemplate, 6).join('\n')); +/** + * Get number of indent spaces for given level + * + * @param {*} indentLevel Indention level + * @param {*} spacesPerLevel Number of spaces per level + * @returns Total number of space + */ +function getIndentSpacing(indentLevel, spacesPerLevel = 2) { + return indentLevel * spacesPerLevel; } function isMsgInterface(rosInterface) { @@ -451,7 +542,16 @@ function isValidAction(rosActionInterface, infos) { return matches === SUCCESS_MATCH_COUNT; } -function fieldType2JSName(fieldInfo, subFolder = 'msg') { +function fieldType2JSName( + fieldInfo, + subFolder = 'msg', + willGenerateDescriptorInterface = false +) { + if (willGenerateDescriptorInterface) { + return fieldInfo.type.isPrimitiveType + ? `${fieldInfo.type.type}` + : `${fieldInfo.type.pkgName}/${subFolder}/${fieldInfo.type.type}`; + } return fieldInfo.type.isPrimitiveType ? primitiveType2JSName(fieldInfo.type.type) : `${fieldInfo.type.pkgName}.${subFolder}.${fieldInfo.type.type}`; diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index 7597ccd8..e6ed1e1d 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -1,6 +1,6 @@ /// -import { expectType } from 'tsd'; +import { expectType, expectAssignable } from 'tsd'; import * as rclnodejs from 'rclnodejs'; const NODE_NAME = 'test_node'; @@ -390,3 +390,40 @@ param.value.integer_value = BigInt(123); expectType(param.value.integer_value); param.value.byte_array_value = [1, 2, 3]; expectType(param.value.byte_array_value); + +// ---- Descriptors ----- +// Note: All fields are of type string exactly equal to the type of interface. +// built-in msg +const duration = rclnodejs.createMessageObject( + 'builtin_interfaces/msg/descriptor/Duration' +); +expectType(duration); +expectAssignable<'int32'>(duration.sec); +expectAssignable<'uint32'>(duration.nanosec); +// msg containing complex types +const poseStampedDescriptor = rclnodejs.createMessageObject( + 'geometry_msgs/msg/descriptor/PoseStamped' +); +expectType( + poseStampedDescriptor +); +expectAssignable<'std_msgs/msg/Header'>(poseStampedDescriptor.header); +expectAssignable<'geometry_msgs/msg/Pose'>(poseStampedDescriptor.pose); +// action interface +const fibonacciFeedback = rclnodejs.createMessageObject( + 'example_interfaces/action/descriptor/Fibonacci_Feedback' +); +expectType( + fibonacciFeedback +); +expectAssignable<'int32[]'>(fibonacciFeedback.sequence); +// srv interface +const cancelGoalRequestDescriptor = rclnodejs.createMessageObject( + 'action_msgs/srv/descriptor/CancelGoal_Request' +); +expectType( + cancelGoalRequestDescriptor +); +expectAssignable<'action_msgs/msg/GoalInfo'>( + cancelGoalRequestDescriptor.goal_info +);