rclnodejs
Version:
ROS2.0 JavaScript client with Node.js
712 lines (619 loc) • 20.5 kB
JavaScript
// 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.
// create interfaces.d.ts containing from each typeclass definition
/* Example output for std_msgs_msg_String
declare module "rclnodejs" {
namespace std_msgs {
namespace msg {
export type String = {
data: string
}
}
}
}
*/
;
const os = require('os');
const path = require('path');
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/');
const pkgInfos = getPkgInfos(generatedPath);
if (pkgInfos.length === 0) {
console.log('No package found, prebuild interfaces.d.ts will be used.');
return;
}
// 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);
}
// scan generated files, i.e., rootDir, and collect pkg and ROS2 interface info
function getPkgInfos(rootDir) {
let pkgInfos = [];
let pkgs = fs.readdirSync(rootDir);
for (let pkg of pkgs) {
if (pkg.endsWith('.json')) continue;
const pkgInfo = {
name: pkg,
subfolders: new Map(),
};
const pkgPath = path.join(rootDir, pkg);
const files = fs.readdirSync(pkgPath).filter((fn) => fn.endsWith('.js'));
for (let filename of files) {
const typeClass = fileName2Typeclass(filename);
if (
!typeClass.type ||
pkgFilters.matchesAny({
pkgName: typeClass.package,
interfaceName: typeClass.name,
})
)
continue;
let rosInterface = null;
try {
rosInterface = loader.loadInterface(typeClass);
} catch (e) {
console.log(
`${e.message}, please make sure it's built for ${os.arch()} platform correctly.`
);
return pkgInfos;
}
if (!pkgInfo.subfolders.has(typeClass.type)) {
pkgInfo.subfolders.set(typeClass.type, []);
}
pkgInfo.subfolders.get(typeClass.type).push(rosInterface);
}
pkgInfos.push(pkgInfo);
}
return pkgInfos;
}
function savePkgInfoAsTSD(pkgInfos, fd) {
const messagesMap = {
string: 'string',
};
const servicesMap = {};
const actionsMap = {};
fs.writeSync(fd, '/* eslint-disable camelcase */\n');
fs.writeSync(fd, '/* eslint-disable max-len */\n');
fs.writeSync(fd, '// DO NOT EDIT\n');
fs.writeSync(fd, '// This file is generated by the rostsd_gen script\n\n');
fs.writeSync(fd, "declare module 'rclnodejs' {\n");
for (const pkgInfo of pkgInfos) {
if (pkgInfo.subfolders.size === 0) continue;
// write namespaces heirarchy for package
fs.writeSync(fd, ` namespace ${pkgInfo.name} {\n`);
for (const subfolder of pkgInfo.subfolders.keys()) {
fs.writeSync(fd, ` namespace ${subfolder} {\n`);
// 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');
}
// close pkg level namespace declare
fs.writeSync(fd, ' }\n\n');
}
// write messages type mappings
fs.writeSync(fd, ' type MessagesMap = {\n');
for (const key in messagesMap) {
fs.writeSync(fd, ` '${key}': ${messagesMap[key]},\n`);
}
fs.writeSync(fd, ' };\n');
fs.writeSync(fd, ' type MessageTypeClassName = keyof MessagesMap;\n');
fs.writeSync(fd, ' type Message = MessagesMap[MessageTypeClassName];\n');
fs.writeSync(
fd,
' type MessageType<T> = T extends MessageTypeClassName ? MessagesMap[T] : object;\n\n'
);
// write message contructor mappings
fs.writeSync(fd, ' type MessageTypeClassConstructorMap = {\n');
for (const key in messagesMap) {
if (key === 'string') {
fs.writeSync(fd, " 'string': never,\n");
continue;
}
fs.writeSync(fd, ` '${key}': ${messagesMap[key]}Constructor,\n`);
}
fs.writeSync(fd, ' };\n');
fs.writeSync(
fd,
' type MessageConstructorType<T> = ' +
'T extends MessageTypeClassName ? MessageTypeClassConstructorMap[T] : object;\n\n'
);
// write services type mappings
fs.writeSync(fd, ' type ServicesMap = {\n');
for (const key in servicesMap) {
fs.writeSync(fd, ` '${key}': ${servicesMap[key]},\n`);
}
fs.writeSync(fd, ' };\n');
fs.writeSync(fd, ' type ServiceTypeClassName = keyof ServicesMap;\n');
fs.writeSync(fd, ' type Service = ServicesMap[ServiceTypeClassName];\n');
fs.writeSync(
fd,
' type ServiceType<T> = T extends ServiceTypeClassName ? ServicesMap[T] : object;\n\n'
);
// write actions type mappings
fs.writeSync(fd, ' type ActionsMap = {\n');
for (const key in actionsMap) {
fs.writeSync(fd, ` '${key}': ${actionsMap[key]},\n`);
}
fs.writeSync(fd, ' };\n');
fs.writeSync(fd, ' type ActionTypeClassName = keyof ActionsMap;\n');
fs.writeSync(fd, ' type Action = ActionsMap[ActionTypeClassName];\n');
fs.writeSync(
fd,
' type ActionType<T> = T extends ActionTypeClassName ? ActionsMap[T] : object;\n\n'
);
fs.writeSync(
fd,
' type TypeClassName = MessageTypeClassName | ServiceTypeClassName | ActionTypeClassName;\n'
);
fs.writeSync(
fd,
' type InterfaceType<T> = T extends TypeClassName ? ' +
'(MessageTypeClassConstructorMap & ServicesMap & ActionsMap)[T] : object;\n'
);
// close module declare
fs.writeSync(fd, '}\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);
const innerIndentLevel = indentLevel + 1;
const innerIndentSpacing = getIndentSpacing(innerIndentLevel);
saveMsgFieldsAsTSD(
rosMsgInterface,
fd,
innerIndentSpacing,
';',
'',
useSamePkg,
willGenerateDescriptorInterface
);
const tmplEnd = indentString('}\n', outerIndentSpacing);
fs.writeSync(fd, tmplEnd);
}
/**
* Writes the message fields as typescript definitions.
*
* @param {*} rosMsgInterface ros message
* @param {*} fd file descriptor
* @param {string} indent The amount of indent, in spaces
* @param {string} lineEnd The character to put at the end of each line, usually ','
* or ';'
* @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}
*/
function saveMsgFieldsAsTSD(
rosMsgInterface,
fd,
indent = 0,
lineEnd = ',',
typePrefix = '',
useSamePackageSubFolder = false,
willGenerateDescriptorInterface = false
) {
let type = rosMsgInterface.type();
let fields = rosMsgInterface.ROSMessageDef.fields;
for (const field of fields) {
let subFolder =
useSamePackageSubFolder && field.type.pkgName === type.pkgName
? type.subFolder
: 'msg';
let fieldType = fieldType2JSName(
field,
subFolder,
willGenerateDescriptorInterface
);
let tp = field.type.isPrimitiveType ? '' : typePrefix;
if (typePrefix === 'rclnodejs.') {
fieldType = 'any';
tp = '';
}
let arrayString = '';
if (field.type.isArray) {
arrayString = '[]';
if (field.type.isFixedSizeArray && willGenerateDescriptorInterface) {
arrayString = `[${field.type.arraySize}]`;
}
if (fieldType === 'number' && !willGenerateDescriptorInterface) {
// for number[] include alternate typed-array types, e.g., number[] | uint8[]
let jsTypedArrayName = fieldTypeArray2JSTypedArrayName(field.type.type);
if (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, indentLevel = 3) {
const type = rosMsgInterface.type();
const msgName = type.interfaceName;
let interfaceTmpl = [`export interface ${msgName}Constructor {`];
for (const constant of rosMsgInterface.ROSMessageDef.constants) {
const constantType = primitiveType2JSName(constant.type);
interfaceTmpl.push(` readonly ${constant.name}: ${constantType};`);
}
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, indentLevel = 3) {
const serviceName = rosSrvInterface.type().interfaceName;
const interfaceTemplate = [
`export interface ${serviceName}Constructor extends ROSService {`,
` readonly Request: ${serviceName}_RequestConstructor;`,
` readonly Response: ${serviceName}_ResponseConstructor;`,
'}',
'',
];
const indentSpacing = getIndentSpacing(indentLevel);
fs.writeSync(fd, indentLines(interfaceTemplate, indentSpacing).join('\n'));
}
function saveActionAsTSD(rosActionInterface, fd, indentLevel = 3) {
const actionName = rosActionInterface.type().interfaceName;
const interfaceTemplate = [
`export interface ${actionName}Constructor {`,
` readonly Goal: ${actionName}_GoalConstructor;`,
` readonly Result: ${actionName}_ResultConstructor;`,
` readonly Feedback: ${actionName}_FeedbackConstructor;`,
'}',
'',
];
const indentSpacing = getIndentSpacing(indentLevel);
fs.writeSync(fd, indentLines(interfaceTemplate, indentSpacing).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) {
return rosInterface.hasOwnProperty('ROSMessageDef');
}
function isInternalActionMsgInterface(rosMsgInterface) {
let name = rosMsgInterface.type().interfaceName;
return (
name.endsWith('_FeedbackMessage') ||
name.endsWith('_SendGoal_Request') ||
name.endsWith('_SendGoal_Response') ||
name.endsWith('_GetResult_Request') ||
name.endsWith('_GetResult_Response')
);
}
function isInternalServiceEventMsgInterface(rosMsgInterface) {
let name = rosMsgInterface.type().interfaceName;
let subFolder = rosMsgInterface.type().subFolder;
// Some package puts .srv files under srvs/, e.g., slam_toolbox.
return (
(subFolder == 'srv' || subFolder == 'srvs' || subFolder == 'action') &&
name.endsWith('_Event')
);
}
function isSrvInterface(rosInterface) {
return (
rosInterface.hasOwnProperty('Request') &&
rosInterface.hasOwnProperty('Response')
);
}
function isInternalActionSrvInterface(rosInterface) {
if (!isSrvInterface(rosInterface)) return false;
let name = rosInterface.type().interfaceName;
return name.endsWith('_GetResult') || name.endsWith('_SendGoal');
}
function isActionInterface(rosInterface, pkgInfos) {
return (
rosInterface.hasOwnProperty('Feedback') &&
rosInterface.hasOwnProperty('Goal') &&
rosInterface.hasOwnProperty('Result')
);
}
function isValidService(rosSrvInterface, infos) {
if (!isSrvInterface(rosSrvInterface)) return false;
let serviceName = rosSrvInterface.type().interfaceName;
let requestMsgName = serviceName + '_Request';
let responseMsgName = serviceName + '_Response';
let matches = infos.reduce((matchCnt, info) => {
let infoInterfaceName = info.type().interfaceName;
if (
requestMsgName === infoInterfaceName ||
responseMsgName === infoInterfaceName
) {
matchCnt++;
}
return matchCnt;
}, 0);
return matches === 2;
}
function isValidAction(rosActionInterface, infos) {
if (!isActionInterface(rosActionInterface)) return false;
let actionName = rosActionInterface.type().interfaceName;
let feedback = actionName + '_Feedback';
let feedbackMsg = actionName + '_FeedbackMessage';
let goalMsg = actionName + '_Goal';
let resultMsg = actionName + '_Result';
let getResultSrv = actionName + '_GetResult';
let sendGoalSrv = actionName + '_SendGoal';
let searches = [actionName, feedback, feedbackMsg, goalMsg, resultMsg];
const SUCCESS_MATCH_COUNT = searches.length + 2;
let matches = infos.reduce((matchCnt, info) => {
let infoInterfaceName = info.type().interfaceName;
if (
searches.indexOf(infoInterfaceName) >= 0 ||
(getResultSrv === infoInterfaceName && isValidService(info, infos)) ||
(sendGoalSrv === infoInterfaceName && isValidService(info, infos))
) {
matchCnt++;
}
return matchCnt;
}, 0);
return matches === SUCCESS_MATCH_COUNT;
}
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}`;
}
// https://design.ros2.org/articles/idl_interface_definition.html
// https://github.com/ros2/rosidl/blob/master/rosidl_parser/rosidl_parser/definition.py
function primitiveType2JSName(type) {
let jsName;
switch (type) {
case 'char':
case 'byte':
case 'octet':
// signed explicit integer types
case 'short':
case 'long':
case 'long long':
// unsigned nonexplicit integer types
case 'unsigned short':
case 'unsigned long':
case 'unsigned long long':
// float point types
case 'float':
case 'double':
case 'long double':
// signed explicit integer types
case 'int8':
case 'int16':
case 'int32':
// signed explicit float types
case 'float32':
case 'float64':
// unsigned explicit integer types
case 'uint8':
case 'uint16':
case 'uint32':
jsName = 'number';
break;
case 'bool':
case 'boolean':
jsName = 'boolean';
break;
case 'string':
case 'wstring':
jsName = 'string';
break;
case 'int64':
case 'uint64':
jsName = 'bigint';
break;
}
return jsName;
}
function fieldTypeArray2JSTypedArrayName(type) {
let jsName;
switch (type) {
case 'byte':
case 'octet':
case 'uint8':
jsName = 'Uint8Array';
break;
case 'char':
case 'int8':
jsName = 'Int8Array';
break;
case 'int16':
case 'short':
jsName = 'Int16Array';
break;
case 'uint16':
case 'unsigned short':
jsName = 'Uint16Array';
break;
case 'int32':
case 'long':
jsName = 'Int32Array';
break;
case 'uint32':
case 'unsigned long':
jsName = 'Uint32Array';
break;
case 'float':
case 'float32':
jsName = 'Float32Array';
break;
case 'double':
case 'float64':
jsName = 'Float64Array';
break;
case 'long long':
case 'unsigned long long':
case 'int64':
case 'uint64':
// number
break;
}
return jsName;
}
// example filename: std_msgs_msg_String, sensor_msgs_msg_LaserScan
// result {package: 'std_msgs', type: 'msg', name: 'String'}
function fileName2Typeclass(filename) {
const regex = /(.+)__(\w+)__(\w+)\.js/;
const array = filename.split(regex).filter(Boolean);
if (!array || array.length != 3) {
// todo: throw error
console.log('ERRORRROOROR', array);
return;
}
return {
package: array[0],
type: array[1],
name: array[2],
};
}
function indentString(string, amount) {
if (!string) {
return '';
}
return ' '.repeat(amount) + string;
}
function indentLines(lines, amount) {
if (!Array.isArray(lines)) {
throw new Error('lines must be an array');
}
return lines.map((line) => indentString(line, amount));
}
async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const tsdGenerator = {
generateAll,
};
module.exports = tsdGenerator;