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
+);