diff --git a/rostsd_gen/index.js b/rostsd_gen/index.js index 3fb8fcbf..8f9c7e5a 100644 --- a/rostsd_gen/index.js +++ b/rostsd_gen/index.js @@ -38,7 +38,7 @@ function generateAll() { // load pkg and interface info (msgs and srvs) 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'); @@ -56,17 +56,19 @@ function getPkgInfos(generatedRoot) { const pkgInfo = { name: pkg, - messages: [] + messages: [], + services: [] }; const pkgPath = path.join(rootDir, pkg); - const files = fs.readdirSync(pkgPath); + const files = fs.readdirSync(pkgPath).filter(fn => fn.endsWith('.js')); for (let filename of files) { const typeClass = fileName2Typeclass(filename); if (typeClass.type === 'srv') { // skip __srv__ if (!typeClass.name.endsWith('Request') && !typeClass.name.endsWith('Response')) { + pkgInfo.services.push(typeClass); continue; } } @@ -93,10 +95,12 @@ function getPkgInfos(generatedRoot) { function savePkgInfoAsTSD(pkgInfos, fd) { - - let fullMessageNames = ['string']; + let messagesMap = { + string: 'string', + }; 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'); @@ -115,7 +119,7 @@ function savePkgInfoAsTSD(pkgInfos, fd) { for (const msgInfo of pkgInfo.messages) { if (msgInfo.typeClass.type != curNS) { - if (curNS) { // close current ns + if (curNS) { // close current ns fs.writeSync(fd, ' }\n'); } @@ -129,10 +133,11 @@ function savePkgInfoAsTSD(pkgInfos, fd) { } saveMsgInfoAsTSD(msgInfo, fd); + saveMsgWrapperAsTSD(msgInfo, fd); // full path to this msg - const fullMessageName = `${pkgInfo.name}.${msgInfo.typeClass.type}.${msgInfo.typeClass.name}`; - fullMessageNames.push(fullMessageName); + const fullMessageName = `${pkgInfo.name}/${msgInfo.typeClass.type}/${msgInfo.typeClass.name}`; + messagesMap[fullMessageName] = `${pkgInfo.name}.${msgInfo.typeClass.type}.${msgInfo.typeClass.name}`; } if (curNS) { @@ -144,66 +149,112 @@ function savePkgInfoAsTSD(pkgInfos, fd) { fs.writeSync(fd, ' }\n\n'); } - // write type alias for Message - // e.g. type Message = - // string | - // std_msgs.msg.Bool | - // std_msgs.msg.Byte | - // ... - fs.writeSync(fd, ' type Message = \n'); - for (let i=0; i < fullMessageNames.length; i++) { - fs.writeSync(fd, ' ' + fullMessageNames[i]); - if (i != fullMessageNames.length-1) { - fs.writeSync(fd, ' |\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 extends MessageTypeClassName ? MessagesMap[T] : object;\n\n'); + + // write message wrappers mappings + fs.writeSync(fd, ' type MessageTypeClassWrappersMap = {\n'); + for (const key in messagesMap) { + if (key === 'string') { + fs.writeSync(fd, " 'string': never,\n"); + continue; + } + fs.writeSync(fd, ` '${key}': ${messagesMap[key]}_WrapperType,\n`); + } + fs.writeSync(fd, ' };\n'); + fs.writeSync(fd, ' type MessageWrapperType = T extends MessageTypeClassName ? MessageTypeClassWrappersMap[T] : object;\n\n'); + + // write service type class string + const services = []; + for (const pkg of pkgInfos) { + services.push(...pkg.services); + } + if (!services.length) { + fs.writeSync(fd, ' type ServiceTypeClassName = never;\n\n'); + } else { + fs.writeSync(fd, ' type ServiceTypeClassName = \n'); + for (let i = 0; i < services.length; i++) { + const srv = services[i]; + const srvTypeClassStr = `${srv.package}/${srv.type}/${srv.name}`; + fs.writeSync(fd, ` '${srvTypeClassStr}'`); + + if (i !== services.length - 1) { + fs.writeSync(fd, ' |\n'); + } + } + fs.writeSync(fd, ';\n\n'); } - - fs.writeSync(fd, ';\n'); + + fs.writeSync(fd, ' type TypeClassName = MessageTypeClassName | ServiceTypeClassName;\n'); // close module declare fs.writeSync(fd, '}\n'); + fs.closeSync(fd); } -function saveMsgInfoAsTSD(msgInfo, fd) { - - // write type = xxxx { - const typeTemplate = - ` export type ${msgInfo.typeClass.name} = {\n`; - - fs.writeSync(fd, typeTemplate); - - // write constant definitions - for (let i = 0; i < msgInfo.def.constants.length; i++) { - const constant = msgInfo.def.constants[i]; +function saveMsgWrapperAsTSD(msgInfo, fd) { + const msgName = msgInfo.typeClass.name; + fs.writeSync(fd, ` export type ${msgName}_WrapperType = {\n`); + for (const constant of msgInfo.def.constants) { const constantType = primitiveType2JSName(constant.type); - const tmpl = (constantType == 'string') ? - ` ${constant.name}: '${constant.value}'` : - ` ${constant.name}: ${constant.value}`; - fs.writeSync(fd, tmpl); - - if (i != msgInfo.def.constants.length - 1) { - fs.writeSync(fd, ',\n'); - } else if (msgInfo.def.fields.length > 0) { - fs.writeSync(fd, ',\n'); - } + fs.writeSync(fd, ` readonly ${constant.name}: ${constantType},\n`); } + fs.writeSync(fd, ` new(other?: ${msgName}): ${msgName},\n`); + fs.writeSync(fd, ' }\n'); +} - // write field definitions + +/** + * Writes the message fields as typescript definitions. + * + * @param {*} msgInfo ros message info + * @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 + * @returns {undefined} + */ +function saveMsgFieldsAsTSD(msgInfo, fd, indent=0, lineEnd=',', typePrefix='') { + const indentStr = ' '.repeat(indent); for (let i = 0; i < msgInfo.def.fields.length; i++) { const field = msgInfo.def.fields[i]; - const fieldType = fieldType2JSName(field); - const tmpl = ` ${field.name}: ${fieldType}`; + let fieldType = fieldType2JSName(field); + let tp = field.type.isPrimitiveType ? '' : typePrefix; + if (typePrefix === 'rclnodejs.') { + fieldType = 'any'; + tp = ''; + } + const tmpl = `${indentStr}${field.name}: ${tp}${fieldType}`; fs.writeSync(fd, tmpl); if (field.type.isArray) { fs.writeSync(fd, '[]'); } - if (i != msgInfo.def.fields.length - 1) { - fs.writeSync(fd, ','); - } + fs.writeSync(fd, lineEnd); fs.writeSync(fd, '\n'); } +} + + +function saveMsgInfoAsTSD(msgInfo, fd) { + // write type = xxxx { + const typeTemplate = + ` export type ${msgInfo.typeClass.name} = {\n`; + + fs.writeSync(fd, typeTemplate); + + // write field definitions + saveMsgFieldsAsTSD(msgInfo, fd, 8); // end of def fs.writeSync(fd, ' };\n'); @@ -223,7 +274,7 @@ function primitiveType2JSName(type) { switch (type) { case 'char': case 'byte': - case 'uin8': + case 'uint8': case 'int8': case 'int16': case 'uint16': @@ -256,7 +307,7 @@ function fileName2Typeclass(filename) { const array = filename.split(regex).filter(Boolean); if (!array || array.length != 3) { - // todo: throw error + // todo: throw error console.log('ERRORRROOROR', array); return; } diff --git a/types/index.d.ts b/types/index.d.ts index cd62e535..f3ea705e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,15 +1,14 @@ - // eslint-disable-next-line spaced-comment -/// +/// declare module 'rclnodejs' { /** * Create a node. - * + * * @remarks * See {@link Node} - * + * * @param nodeName - The name used to register in ROS. * @param namespace - The namespace used in ROS, default is an empty string. * @param context - The context, default is Context.defaultContext(). @@ -19,7 +18,7 @@ declare module 'rclnodejs' { /** * Init the module. - * + * * @param context - The context, default is Context.defaultContext(). * @returns A Promise. */ @@ -27,7 +26,7 @@ declare module 'rclnodejs' { /** * Spin up the node event loop to check for incoming events. - * + * * @param node - The node to be spun. * @param timeout - ms to wait, block forever if negative, return immediately when 0, default is 10. */ @@ -35,44 +34,45 @@ declare module 'rclnodejs' { /** * Stop all activity, destroy all nodes and node components. - * + * * @param context - The context, default is Context.defaultContext() */ function shutdown(context?: Context): void; /** * Test if the module is shutdown. - * + * * @returns True if the module is shut down, otherwise return false. */ function isShutdown(): boolean; /** * Get the interface package, which is used by publisher/subscription or client/service. - * + * * @param name - The name of interface to be required. * @returns The object of the required package/interface. */ + function require(name: T): MessageWrapperType; function require(name: string): object; /** * Generate JavaScript structs files from the IDL of - * messages(.msg) and services(.srv). + * messages(.msg) and services(.srv). * Search packages which locate under path $AMENT_PREFIX_PATH - * and output JS files into the 'generated' folder. + * and output JS files into the 'generated' folder. * Any existing files under the generated folder will * be overwritten. - * + * * @returns A Promise. */ function regenerateAll(): Promise; /** - * Judge if the topic or service is hidden, - * + * Judge if the topic or service is hidden, + * * @remarks * See {@link http://design.ros2.org/articles/topic_and_service_names.html#hidden-topic-or-service-names} - * + * * @param name - Name of topic or service. * @returns True if a given topic or service name is hidden, otherwise False. */ @@ -80,7 +80,7 @@ declare module 'rclnodejs' { /** * Expand a given topic name using given node name and namespace. - * + * * @param topicName - Topic name to be expanded. * @param nodeName - Name of the node that this topic is associated with. * @param nodeNamespace - Namespace that the topic is within. @@ -91,7 +91,7 @@ declare module 'rclnodejs' { /** * Create a plain JavaScript message object. - * + * * @param type - type identifier, acceptable formats could be 'std_msgs/std/String' * or {package: 'std_msgs', type: 'msg', name: 'String'} * @returns A Message object or undefined if type is not recognized. diff --git a/types/node.d.ts b/types/node.d.ts index 2d055ded..80c01f0f 100644 --- a/types/node.d.ts +++ b/types/node.d.ts @@ -2,12 +2,12 @@ import { QoS } from 'rclnodejs'; declare module 'rclnodejs' { - /** - * Identifies type of ROS message such as msg or srv. + /** + * Identifies type of ROS message such as msg or srv. */ type TypeClass = (() => any) | - string | // a string representing the message class, e.g. 'std_msgs/msg/String', + TypeClassName | // a string representing the message class, e.g. 'std_msgs/msg/String', { // object representing a message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'} package: string; type: string; @@ -17,8 +17,8 @@ declare module 'rclnodejs' { /** * Configuration options when creating new Publishers, Subscribers, - * Clients and Services. - * + * Clients and Services. + * * See {@link DEFAULT_OPTIONS} */ type Options = { @@ -29,7 +29,7 @@ declare module 'rclnodejs' { /** * A service response to a client request. - * + * * @remarks * You can use {@link response.template | response.template} to get an empty result message. */ @@ -49,9 +49,9 @@ declare module 'rclnodejs' { /** * Send this response to client (the service caller) - * + * * @param response - Response message. - * + * * @remarks * see {@link Response.template} */ @@ -60,9 +60,9 @@ declare module 'rclnodejs' { /** * A callback for receiving published messages. - * + * * @param message - The published message. - * + * * @remarks * See {@link Node#createSubscription | Node.createSubscription} * See {@link Node#createPublisher | Node.createPublisher} @@ -70,20 +70,20 @@ declare module 'rclnodejs' { * See {@link Subscription} */ type SubscriptionCallback = ( - // * @param message - The published message + // * @param message - The published message (message: Message) => void ); /** * Callback for receiving service requests from a client. - * + * * @param request - The request sent to the service * @param response - The response to the client. - * + * * @remarks * Use {@link Response.send | response.send()} to send response object to client - * + * * See {@link Node.createService | Node.createService} * See {@link Client.sendRequest | Client.sendRequest} * See {@link Client} @@ -97,7 +97,7 @@ declare module 'rclnodejs' { /** * Callback for receiving periodic interrupts from a Timer. - * + * * @remarks * See {@link Node.createTimer | Node.createTimer} * See {@link Timer} @@ -107,11 +107,11 @@ declare module 'rclnodejs' { ); /** - * Standard result of Node.getXXXNamesAndTypes() queries + * Standard result of Node.getXXXNamesAndTypes() queries * * @example * ``` - * [ + * [ * { name: '/rosout', types: [ 'rcl_interfaces/msg/Log' ] }, * { name: '/scan', types: [ 'sensor_msgs/msg/LaserScan' ] }, * { name: '/topic', types: [ 'std_msgs/msg/String' ] } @@ -125,15 +125,15 @@ declare module 'rclnodejs' { /** - * Node is the primary entrypoint in a ROS system for communication. - * It can be used to create ROS entities such as publishers, subscribers, + * Node is the primary entrypoint in a ROS system for communication. + * It can be used to create ROS entities such as publishers, subscribers, * services, clients and timers. */ class Node { /** * Get the name of the node. - * + * * @returns The node name. */ name(): string; @@ -147,7 +147,7 @@ declare module 'rclnodejs' { /** * Create a Timer. - * + * * @param period - Elapsed time between interrupt events (milliseconds). * @param callback - Called on timeout interrupt. * @param context - Context, default is Context.defaultContext(). @@ -157,7 +157,7 @@ declare module 'rclnodejs' { /** * Create a Publisher. - * + * * @param typeClass - Type of message that will be published. * @param topic - Name of the topic the publisher will publish to. * @param options - Configuration options, see DEFAULT_OPTIONS @@ -167,7 +167,7 @@ declare module 'rclnodejs' { /** * Create a Subscription. - * + * * @param typeClass - Type of ROS messages the subscription will subscribe to * @param topic - Name of the topic the subcription will subscribe to. * @param options - Configuration options, see DEFAULT_OPTIONS @@ -179,7 +179,7 @@ declare module 'rclnodejs' { /** * Create a Client for making server requests. - * + * * @param typeClass - Service type. * @param serviceName - Service name. * @param options - The options argument used to parameterize the client. @@ -189,7 +189,7 @@ declare module 'rclnodejs' { /** * Create a Service. - * + * * @param typeClass - Service type * @param serviceName - Name of the service. * @param options - Configuration options @@ -202,42 +202,42 @@ declare module 'rclnodejs' { /** * Destroy all entities allocated by this node, including - * Timers, Publishers, Subscriptions, Clients, Services + * Timers, Publishers, Subscriptions, Clients, Services * and Timers. */ destroy(): void; /** * Destroy a Publisher. - * + * * @param publisher - Publisher to be destroyed. */ destroyPublisher(publisher: Publisher): void; /** * Destroy a Subscription. - * + * * @param subscription - Subscription to be destroyed. */ destroySubscription(subscription: Subscription): void; /** * Destroy a Client. - * + * * @param client - Client to be destroyed. */ destroyClient(client: Client): void; /** * Destroy a Service. - * + * * @param service - Service to be destroyed. */ destroyService(service: Service): void; /** * Destroy a Timer. - * + * * @param timer - Timer to be destroyed. */ destroyTimer(timer: Timer): void; @@ -245,14 +245,14 @@ declare module 'rclnodejs' { /** * Get a remote node's published topics. - * + * * @param remoteNodeName - Name of a remote node. * @param namespace - Name of the remote namespace. * @param noDemangle - If true, topic names and types returned will not be demangled, default: false. * @returns An array of the names and types. - * [ + * [ * { name: '/rosout', types: [ 'rcl_interfaces/msg/Log' ] }, - * { name: '/scan', types: [ 'sensor_msgs/msg/LaserScan' ] } + * { name: '/scan', types: [ 'sensor_msgs/msg/LaserScan' ] } * ] */ getPublisherNamesAndTypesByNode(remoteNodeName: string, namespace?: string, @@ -260,13 +260,13 @@ declare module 'rclnodejs' { /** * Get a remote node's subscribed topics. - * + * * @param nodeName - Name of the remote node. * @param namespace - Name of the remote namespace. * @param noDemangle - If true topic, names and types returned will not be demangled, default: false. * @returns An array of the names and types. - * [ - * { name: '/topic', types: [ 'std_msgs/msg/String' ] } + * [ + * { name: '/topic', types: [ 'std_msgs/msg/String' ] } * ]s */ getSubscriptionNamesAndTypesByNode(remoteNodeName: string, namespace?: string, @@ -274,37 +274,37 @@ declare module 'rclnodejs' { /** * Get a remote node's service topics. - * + * * @param remoteNodeName - Name of the remote node. * @param namespace - Name of the remote namespace. * @returns An array of the names and types. - * [ + * [ * { name: '/rosout', types: [ 'rcl_interfaces/msg/Log' ] }, - * ... + * ... * ] */ getServiceNamesAndTypesByNode(remoteNodeName: string, namespace?: string): Array; /** * Get this node's topics and corresponding types. - * + * * @param noDemangle - If true. topic names and types returned will not be demangled, default: false. * @returns An array of the names and types. - * [ + * [ * { name: '/rosout', types: [ 'rcl_interfaces/msg/Log' ] }, * { name: '/scan', types: [ 'sensor_msgs/msg/LaserScan' ] }, - * { name: '/topic', types: [ 'std_msgs/msg/String' ] } + * { name: '/topic', types: [ 'std_msgs/msg/String' ] } * ] */ getTopicNamesAndTypes(noDemangle?: boolean): Array; /** * Get this node's service names and corresponding types. - * + * * @returns An array of the names and types. - * [ + * [ * { name: '/start_motor', types: [ 'rplidar_ros/srv/Control' ] }, - * { name: '/stop_motor', types: [ 'rplidar_ros/srv/Control' ] } + * { name: '/stop_motor', types: [ 'rplidar_ros/srv/Control' ] } * ] */ getServiceNamesAndTypes(): Array @@ -316,13 +316,13 @@ declare module 'rclnodejs' { /** * Default options when creating a Node, Publisher, Subscription, Client or Service - * + * * ```ts * { * enableTypedArray: true, * qos: QoS.profileDefault * } - * + * * ``` */ export const DEFAULT_OPTIONS: Options;