├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── appveyor.yml ├── bin └── rosbridge.js ├── data └── example.secret ├── demo ├── README.md ├── css │ ├── component.css │ └── demo.css ├── index.html └── js │ ├── btnstate.js │ ├── controller.js │ ├── log.js │ └── ros2dmap.js ├── examples ├── html │ ├── client.html │ ├── publisher.html │ ├── service.html │ └── subscription.html └── index.js ├── index.js ├── lib ├── bridge.js ├── ref_counting_handle.js ├── resource_provider.js ├── rosauth.js └── subscription_manager.js ├── npm-pack.sh ├── package.json └── test ├── README.md ├── browser ├── html_list.json ├── resources │ ├── LICENSE │ ├── testharness.css │ ├── testharness.js │ └── testharnessreport.js ├── server.js ├── test-example.html ├── test-goal.html ├── test-html.js ├── test-ros.html ├── test-service.html └── test-topic.html └── nodejs ├── protocol ├── entry-client-mode.js ├── entry.js ├── test-advertise-msg.js ├── test-advertise-service.js ├── test-advertise.js ├── test-call-service.js ├── test-op-neg.js ├── test-publish-msg.js ├── test-publish.js ├── test-response-op.js ├── test-ros2-protocol-workflow.js ├── test-service-response.js ├── test-set-level.js ├── test-subscribe-msg.js ├── test-subscribe.js ├── test-unadvertise-service.js ├── test-unadvertise.js └── test-unsubscribe.js └── test-rosauth-internal.js /.eslintignore: -------------------------------------------------------------------------------- 1 | test/browser 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | 5 | globals: 6 | every: true 7 | after: true 8 | constantly: true 9 | 10 | rules: 11 | camelcase: [2, {properties: "always"}] 12 | comma-dangle: 0 13 | comma-spacing: [2, {before: false, after: true}] 14 | comma-style: [2, "last"] 15 | handle-callback-err: [2, "^.*(e|E)rr" ] 16 | indent: [2, 2] 17 | key-spacing: [2, { beforeColon: false, afterColon: true }] 18 | max-depth: [1, 6] 19 | max-len: ["error", {"code": 120, "tabWidth": 2, "ignoreComments": true}] 20 | max-nested-callbacks: [1, 7] 21 | no-cond-assign: 2 22 | no-constant-condition: 2 23 | no-dupe-args: 2 24 | no-dupe-keys: 2 25 | no-else-return: 2 26 | no-empty: 2 27 | no-lonely-if: 2 28 | no-multiple-empty-lines: 2 29 | no-nested-ternary: 2 30 | no-self-compare: 2 31 | no-sync: 1 32 | no-throw-literal: 2 33 | no-underscore-dangle: 0 34 | quote-props: [2, "as-needed"] 35 | quotes: [2, "single", {"avoidEscape": true}] 36 | radix: 2 37 | semi-spacing: [2, {before: false, after: true}] 38 | semi: [2, "always"] 39 | keyword-spacing: [2, {before: true, after: true}] 40 | space-before-blocks: [2, "always"] 41 | space-before-function-paren: [1, "never"] 42 | space-in-parens: [2, "never"] 43 | spaced-comment: [1, "always"] 44 | strict: [2, "global"] 45 | valid-jsdoc: 2 46 | yoda: [2, "never"] 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | .*.sw? 4 | *.gypcmd 5 | *.mk 6 | *.pyc 7 | *.tar.gz 8 | *.log 9 | build 10 | cscope.* 11 | gen 12 | node_modules 13 | npm-debug.log 14 | tags 15 | cpplint.py 16 | generated 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | 4 | sudo: required 5 | 6 | branches: 7 | only: 8 | - develop 9 | - master 10 | 11 | before_install: 12 | - sudo docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 13 | - sudo docker pull ubuntu:bionic 14 | - sudo docker build -t rcldocker . 15 | 16 | script: 17 | - sudo docker run -v $(pwd):/root/ros2-web-bridge --rm rcldocker bash -i -c 'cd /root/ros2-web-bridge && npm install --unsafe-perm && npm run lint && npm run ci' 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:focal 2 | 3 | ENV GIT_USER_NAME mrbuild 4 | ENV GIT_USER_EMAIL mrbuild@github.com 5 | 6 | RUN apt update && apt install -y git locales python curl wget 7 | RUN locale-gen en_US en_US.UTF-8 && update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 8 | ENV LANG en_US.UTF-8 9 | 10 | RUN apt install -y gnupg2 lsb-release 11 | RUN curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | apt-key add - 12 | RUN sh -c 'echo "deb [arch=amd64] http://packages.ros.org/ros2/ubuntu `lsb_release -cs` main" > /etc/apt/sources.list.d/ros2-latest.list' 13 | 14 | # Install prerequisites 15 | RUN export DEBIAN_FRONTEND=noninteractive && apt update && apt install -y \ 16 | build-essential \ 17 | python3-colcon-common-extensions \ 18 | python3-rosdep \ 19 | libssl-dev \ 20 | cppcheck 21 | 22 | RUN rosdep init 23 | RUN rosdep update 24 | 25 | # Configure git 26 | RUN git config --global user.name $GIT_USER_NAME \ 27 | && git config --global user.email $GIT_USER_EMAIL 28 | 29 | # Get ROS2 latest package 30 | ENV ROS2_WS=/root 31 | WORKDIR $ROS2_WS 32 | 33 | RUN wget https://github.com/ros2/ros2/releases/download/release-foxy-20201211/ros2-foxy-20201211-linux-focal-amd64.tar.bz2 \ 34 | && tar xf ros2-foxy-20201211-linux-focal-amd64.tar.bz2 35 | 36 | # [Ubuntu 20.04] 37 | RUN rosdep install --from-paths $ROS2_WS/ros2-linux/share --ignore-src --rosdistro foxy -y --skip-keys "console_bridge fastcdr fastrtps osrf_testing_tools_cpp poco_vendor rmw_connext_cpp rosidl_typesupport_connext_c rosidl_typesupport_connext_cpp rti-connext-dds-5.3.1 tinyxml_vendor tinyxml2_vendor urdfdom urdfdom_headers" 38 | 39 | RUN echo "source $ROS2_WS/ros2-linux/local_setup.bash" >> $HOME/.bashrc 40 | 41 | # Install nvm, Node.js and node-gyp 42 | ENV NODE_VERSION v12.20.0 43 | RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash \ 44 | && . $HOME/.nvm/nvm.sh \ 45 | && nvm install $NODE_VERSION && nvm alias default $NODE_VERSION 46 | 47 | ENV PATH /bin/versions/node/$NODE_VERSION/bin:$PATH 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ros2-web-bridge [![Build Status](https://travis-ci.org/RobotWebTools/ros2-web-bridge.svg?branch=develop)](https://travis-ci.org/RobotWebTools/ros2-web-bridge)[![Build status](https://ci.appveyor.com/api/projects/status/upb8xbq0f05mtgff/branch/develop?svg=true)](https://ci.appveyor.com/project/minggangw/ros2-web-bridge/branch/develop)[![npm](https://img.shields.io/npm/dt/ros2-web-bridge.svg)](https://www.npmjs.com/package/ros2-web-bridge)[![license](https://img.shields.io/github/license/RobotWebTools/ros2-web-bridge.svg)](https://github.com/RobotWebTools/ros2-web-bridge/blob/develop/LICENSE) 2 | 3 | --- 4 | 5 | ## :warning: Warning :warning: 6 | 7 | This package is not actively maintained. Please use the [rosbridge_suite](https://github.com/RobotWebTools/rosbridge_suite) package instead for your ROS 2 websocket communication needs. 8 | 9 | ## ros2-web-bridge vs rosbridge_suite 10 | 11 | - [`rosbridge_suite`](https://github.com/RobotWebTools/rosbridge_suite) (or `rosbridge_server`) is recommended for communicating with ROS 2 over websockets. It is written in Python and is actively maintained by the ROS web working group. 12 | - [`ros2-web-bridge`](https://github.com/RobotWebTools/ros2-web-bridge) (this project) is an earlier attempt at enabling ROS 2 communication over websockets. It is written in JavaScript, and requires Node.js to be installed on your robot. It cannot be installed via `rosdep` or `apt` like a regular ROS package, and must be cloned and built locally. 13 | 14 | --- 15 | 16 | ## Server Implementations of the rosbridge v2 Protocol 17 | 18 | ros2-web-bridge, which leverages the [rclnodejs](https://github.com/RobotWebTools/rclnodejs) client, provides a JSON interface to [ROS 2](https://index.ros.org/doc/ros2/) by adopting the [rosbridge v2 protocol](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md). The bridge can process commands through JSON tuneled over WebSockets. 19 | 20 | ## ROS 2 support 21 | 22 | The ros2-web-bridge **SUPPORTS** the latest ROS 2 stable release by default (currently [Dashing Patch 2](https://github.com/ros2/ros2/releases/tag/release-dashing-20190806)), please visit the [relase channel](https://github.com/ros2/ros2/releases) to check out the information. 23 | 24 | Any one who wants to run on the nightly build of ROS 2, please change the `dependencies` section of [package.json](https://github.com/RobotWebTools/ros2-web-bridge/blob/develop/package.json) file to install other version of [rclnodejs](https://github.com/RobotWebTools/rclnodejs#match-with-ros-20-stable-releases). 25 | 26 | ## Supported Clients 27 | 28 | A client is a program that communicates with ros2-web-bridge using its JSON API. Clients include: 29 | 30 | * [roslibjs](https://github.com/RobotWebTools/roslibjs) - A JavaScript API, which communicates with ros2-web-bridge over WebSockets. 31 | 32 | ## Install 33 | 34 | 1. Prepare for ROS 2 35 | Please reference the [documentation](https://index.ros.org/doc/ros2/Installation/) to install ROS 2. 36 | 2. Install `Node.js` 37 | You can install Node.js: 38 | * Download from Node.js offical [website](https://nodejs.org/en/), and install it. 39 | * Use the Node Version Manager ([nvm](https://github.com/creationix/nvm)) to install it. 40 | 3. Clone and install dependencies 41 | Note that a ROS 2 installation has to be sourced before installing dependencies. 42 | ```bash 43 | $ git clone https://github.com/RobotWebTools/ros2-web-bridge.git 44 | $ cd ros2-web-bridge 45 | $ source /opt/ros/$DISTRO/setup.sh # or a source installation 46 | $ npm install 47 | ``` 48 | 49 | ## Run Examples 50 | 51 | 1. Make sure to source a ROS 2 installation, e.g.: 52 | ```bash 53 | $ source /opt/ros/$DISTRO/setup.sh # or a source installation 54 | ``` 55 | 2. Start `ros2-web-bridge` module: 56 | ```bash 57 | $ node bin/rosbridge.js 58 | ``` 59 | If you want to start in client mode (i.e. connecting the bridge to an existing websocket server), do this instead: 60 | ```bash 61 | $ node bin/rosbridge.js --address ws://
: 62 | ``` 63 | 3. Start the [express](https://www.npmjs.com/package/express) server: 64 | ```bash 65 | $ cd examples && node index.js 66 | ``` 67 | 4. Open your browser, and navigate to URL: http://localhost:3000/html/publisher.html 68 | 69 | ## Not supported `op` 70 | 71 | Some experimental operations defined by rosbridge v2.0 protocol specification are not supported by ros2-web-bridge now, please check out the list: 72 | 73 | * [fragment](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md#311-fragmentation--fragment--experimental) 74 | * [png](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md#312-png-compression--png--experimental) 75 | * [status](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md#322-status-message--status--experimental) 76 | 77 | and the authentication 78 | 79 | * [auth](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md#331-authenticate--auth-) 80 | 81 | ## Compability with rosbridge v2.0 protocol 82 | 83 | We are trying to obey the [rosbridge v2 protocol](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md), but there are still some operation commands which can not follow the spec. The table below lists the differences: 84 | 85 | opreations | rosbridge v2.0 protocol spec | ros2-web-bridge implementation | 86 | :------------: | :------------ | :------------- | 87 | publish | If the msg is a subset of the type of the topic, then a warning status message is sent and the unspecified fields are filled in with [defaults](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md#343-publish--publish-). | If the subset of the msg is unspecified, then an error status message is sent and this message is dropped. 88 | subscribe | The type of the topic is [optional](https://github.com/RobotWebTools/rosbridge_suite/blob/develop/ROSBRIDGE_PROTOCOL.md#344-subscribe). | The type of the topic must be offered. 89 | 90 | If you use [roslibjs](https://static.robotwebtools.org/roslibjs/current/roslib.js) as the client running in the browser, please reference the code snippet below: 91 | 92 | * Subscribe to a topic. 93 | ```JavaScript 94 | // Define a topic with its type. 95 | var example = new ROSLIB.Topic({ 96 | ros : ros, 97 | name : '/example_topic', 98 | messageType : 'std_msgs/String' 99 | }); 100 | 101 | // Subscribe to a topic. 102 | example.subscribe(function(message) { 103 | console.log(`Receive message: ${message}`); 104 | }); 105 | ``` 106 | 107 | ## Contributing 108 | 109 | If you want to contribute code to this project, first you need to fork the 110 | project. The next step is to send a pull request (PR) for review. The PR will be reviewed by the project team members. Once you have gained "Look Good To Me (LGTM)", the project maintainers will merge the PR. 111 | 112 | ## License 113 | 114 | This project abides by [Apache License 2.0](https://github.com/RobotWebTools/ros2-web-bridge/blob/develop/LICENSE). 115 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | 3 | branches: 4 | only: 5 | - master 6 | - develop 7 | 8 | image: Visual Studio 2019 9 | 10 | environment: 11 | nodejs_version: "12" 12 | PYTHON3: "c:\\Python37-x64" 13 | PYTHON2: "c:\\Python27" 14 | 15 | clone_folder: c:\proj\ros2-web-bridge 16 | 17 | before_build: 18 | - cd c:\ 19 | - md download 20 | - cd download 21 | - choco install -y wget cmake 22 | - choco install -y vcredist140 23 | - "SET PATH=C:\\Program Files\\CMake\\bin;%PATH%" 24 | - appveyor DownloadFile https://github.com/ros2/choco-packages/releases/download/2019-10-24/asio.1.12.1.nupkg 25 | - appveyor DownloadFile https://github.com/ros2/choco-packages/releases/download/2020-02-24/bullet.2.89.0.nupkg 26 | - appveyor DownloadFile https://github.com/ros2/choco-packages/releases/download/2019-10-24/cunit.2.1.3.nupkg 27 | - appveyor DownloadFile https://github.com/ros2/choco-packages/releases/download/2019-10-24/eigen.3.3.4.nupkg 28 | - appveyor DownloadFile https://github.com/ros2/choco-packages/releases/download/2019-10-24/tinyxml-usestl.2.6.2.nupkg 29 | - appveyor DownloadFile https://github.com/ros2/choco-packages/releases/download/2019-10-24/tinyxml2.6.0.0.nupkg 30 | - appveyor DownloadFile https://github.com/ros2/choco-packages/releases/download/2019-10-24/log4cxx.0.10.0.nupkg 31 | - choco install -y -s c:\download\ asio bullet cunit eigen tinyxml-usestl tinyxml2 log4cxx 32 | - appveyor DownloadFile https://github.com/ros2/ros2/releases/download/release-foxy-20201211/ros2-foxy-20201211-windows-release.amd64.zip 33 | - 7z x -y "c:\download\ros2-foxy-20201211-windows-release.amd64.zip" -o"c:\" > nul 34 | - setx -m OPENSSL_CONF C:\OpenSSL-v111-Win64\bin\openssl.cfg 35 | - set PATH=C:\OpenSSL-v111-Win64\bin;%PATH% 36 | - setx AMENT_PYTHON_EXECUTABLE "c:\Python37" 37 | - refreshenv 38 | - "SET PATH=%PYTHON3%;%PYTHON3%\\bin;%PYTHON3%\\Scripts;%PATH%" 39 | - python -m pip install -U catkin_pkg cryptography empy ifcfg lark-parser lxml netifaces numpy opencv-python pyparsing pyyaml setuptools colcon-common-extensions 40 | 41 | build_script: 42 | - cd c:\proj\ros2-web-bridge 43 | - ps: Install-Product node $env:nodejs_version x64 44 | - call c:\ros2-windows\local_setup.bat 45 | - "SET PATH=%PYTHON2%;%PYTHON2%\\bin;%PYTHON2%\\Scripts;%PATH%" 46 | - node --version 47 | - npm --version 48 | - python --version 49 | - npm install 50 | - npm run lint 51 | 52 | test_script: 53 | - cmd: call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat" 54 | - "SET PATH=%PYTHON3%;%PYTHON3%\\bin;%PYTHON3%\\Scripts;C:\\Program Files\\CMake\\bin;%PATH%" 55 | - npm run ci 56 | -------------------------------------------------------------------------------- /bin/rosbridge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | 'use strict'; 18 | 19 | const rosbridge = require('../index.js'); 20 | const app = require('commander'); 21 | const pkg = require('../package.json'); 22 | 23 | app 24 | .version(pkg.version) 25 | .option('-p, --port [port_number]', 'Listen port, default to :9090') 26 | .option('-a, --address [address_string]', 'Remote server address (client mode); server mode if unset') 27 | .option('-r, --retry_startup_delay [delay_ms]', 'Retry startup delay in millisecond') 28 | .option('-o, --fragment_timeout [timeout_ms]', 'Fragment timeout in millisecond') 29 | .option('-d, --delay_between_messages [delay_ms]', 'Delay between messages in millisecond') 30 | .option('-m, --max_message_size [byte_size]', 'Max message size') 31 | .option('-t, --topics_glob [glob_list]', 'A list or None') 32 | .option('-s, --services_glob [glob_list]', 'A list or None') 33 | .option('-g, --params_glob [glob_list]', 'A list or None') 34 | .option('-b, --bson_only_mode', 'Unsupported in WebSocket server, will be ignored') 35 | .option('-l, --status_level [level_string]', 'Status level (one of "error", "warning", "info", "none"; default "error")') 36 | .parse(process.argv); 37 | 38 | rosbridge.createServer(app); 39 | -------------------------------------------------------------------------------- /data/example.secret: -------------------------------------------------------------------------------- 1 | relaxthisain'tmyfirstrodeo -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Web demo app for ROS Conference 2018 3 | 4 | ## Hardware: 5 | * [Intel Up-board Squared](http://www.up-board.org/upsquared/) 6 | * [TurtleBot3 Burger](http://www.robotis.us/turtlebot-3/) 7 | * USB Wi-Fi adpater 8 | * An available wireless network 9 | * Node.js 8.11.4 installed 10 | 11 | ## Pre-condition 12 | * Ubuntu Linux 16.04 installed on Up-board Squared 13 | * ROS2 release package available 14 | 15 | ## Setup 16 | ### Install MicroRTPSAgent 17 | ``` 18 | $ git clone https://github.com/eProsima/micro-RTPS-agent.git 19 | $ cd micro-RTPS-agent 20 | $ mkdir build && cd build 21 | $ cmake -DTHIRDPARTY=ON .. 22 | $ make 23 | $ sudo make install 24 | ``` 25 | 26 | ### Install ros2-web-bridge 27 | ``` 28 | $ source /local_setup.bash 29 | $ git clone https://github.com/RobotWebTools/ros2-web-bridge.git 30 | $ cd ros2-web-bridge 31 | $ npm install 32 | ``` 33 | 34 | ### Install http-server CLI tools 35 | ``` 36 | $ npm install -g http-server 37 | ``` 38 | 39 | ## Run the demo 40 | ### Start up MicroRTPSAgent 41 | New a terminal session, run 42 | ``` 43 | $ cd /usr/local/bin 44 | $ ./MicroRTPSAgent serial /dev/ttyACM0 45 | ``` 46 | 47 | ### Start up the web bridge server 48 | New a terminal session, run 49 | ``` 50 | $ source /local_setup.bash 51 | $ cd ros2-web-bridge 52 | $ export DEBUG=ros2-web-bridge:* 53 | $ node bin/rosbridge.js 54 | ``` 55 | 56 | ### Host the web demo app 57 | New a terminal session and run 58 | ``` 59 | $ cd ros2-web-bridge/demo 60 | $ http-server -c-1 61 | ``` 62 | 63 | ### Try to control the robot remotely 64 | Now you can put the TurtleBot3 robot on the ground. Let another PC be in the same wireless network, then launch the browser and enter: `http://:8080`. After the page loaded, you can press the `Up/Left/Right/Down` buttons to control the robot remotely. 65 | 66 | ## Known limitation: 67 | If you shutdown the MicroRTPSAgent utility, you have to reset the OpenCR before you launch it again. 68 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:after, 3 | *:before { 4 | -webkit-box-sizing: border-box; 5 | -moz-box-sizing: border-box; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | color: #5a5350; 11 | font-weight: 200; 12 | font-family: 'Lato', Calibri, Arial, sans-serif; 13 | overflow-y: scroll; 14 | overflow-x: hidden; 15 | text-shadow: #000 1px 1px; 16 | } 17 | 18 | a { 19 | text-decoration: none; 20 | color: rgba(255, 255, 255, 0.8) !important; 21 | outline: none; 22 | } 23 | 24 | a:hover, 25 | a:focus { 26 | color: rgba(255, 255, 255, 1.0) !important; 27 | text-shadow: #000 1px 2px; 28 | outline: none; 29 | } 30 | 31 | .clearfix:before, 32 | .clearfix:after { 33 | display: table; 34 | content: " "; 35 | } 36 | 37 | .clearfix:after { 38 | clear: both; 39 | } 40 | 41 | .ros-header, 42 | .ros-top { 43 | font-family: 'Lato', Arial, sans-serif; 44 | } 45 | 46 | .ros-header { 47 | margin: 0 auto 1em; 48 | padding: 2em; 49 | text-align: center; 50 | } 51 | 52 | .ros-header h1 { 53 | margin: 0; 54 | font-weight: 300; 55 | font-size: 2.625em; 56 | line-height: 1.2; 57 | } 58 | 59 | .ros-header span { 60 | display: block; 61 | padding: 0 0 0.6em 0.1em; 62 | font-size: 60%; 63 | color: #aca89a; 64 | } 65 | 66 | /* To Navigation Style */ 67 | 68 | .ros-top { 69 | width: 100%; 70 | text-transform: uppercase; 71 | font-size: 0.69em; 72 | line-height: 2.2; 73 | font-weight: 700; 74 | background: rgba(255, 255, 255, 0.3); 75 | } 76 | 77 | .ros-top a { 78 | display: inline-block; 79 | padding: 0 1em; 80 | text-decoration: none; 81 | letter-spacing: 0.1em; 82 | } 83 | 84 | .ros-top a:hover { 85 | background: rgba(255, 255, 255, 0.4); 86 | color: #333; 87 | } 88 | 89 | .ros-top span.right { 90 | float: right; 91 | } 92 | 93 | .ros-top span.right a { 94 | display: block; 95 | float: left; 96 | } 97 | 98 | .twodmap { 99 | border: 1px rgba(255, 255, 255, 0.2); 100 | /* padding: 4em; */ 101 | } 102 | 103 | .main { 104 | max-width: 96em; 105 | margin: 0 auto; 106 | } 107 | 108 | .columnleft { 109 | float: left; 110 | width: 38.2%; 111 | padding: 0 2em; 112 | min-height: 300px; 113 | position: relative; 114 | text-align: center; 115 | } 116 | 117 | .columnright { 118 | float: left; 119 | width: 61.8%; 120 | padding: 0 2em; 121 | min-height: 300px; 122 | position: relative; 123 | text-align: right; 124 | } 125 | 126 | .columnright { 127 | /* box-shadow: -1px 0 0 rgba(0, 0, 0, 0.1); */ 128 | text-align: left; 129 | } 130 | 131 | .controller { 132 | text-align: center; 133 | display: inline-flex; 134 | justify-content: center; 135 | flex-direction: column; 136 | } 137 | 138 | .controller .nx { 139 | text-align: center; 140 | display: inline-flex; 141 | justify-content: center; 142 | flex-direction: row; 143 | } 144 | 145 | .controller .nx div { 146 | width: 100px; 147 | height: 100px; 148 | } 149 | 150 | .playstop { 151 | text-align: center; 152 | margin: 0 auto; 153 | } 154 | 155 | .playstop div { 156 | font-size: 1.5em; 157 | border: 6px solid rgba(255, 255, 255, 0.6); 158 | padding: 0.4em 0.8em 0.5em 0.8em; 159 | width: 180px; 160 | margin: 2em auto; 161 | border-radius: 12px; 162 | font-weight: 600; 163 | color: rgba(255, 255, 255, 0.9); 164 | } 165 | 166 | .playstop div:hover { 167 | color: rgba(255, 255, 255, 1.0); 168 | border: 6px solid rgba(255, 255, 255, 1.0); 169 | cursor: pointer; 170 | font-weight: 800; 171 | } 172 | 173 | svg { 174 | color: rgba(255, 255, 255, 0.6); 175 | } 176 | 177 | svg:hover { 178 | color: rgba(255, 255, 255, 1.0); 179 | cursor: pointer; 180 | } 181 | 182 | svg:active { 183 | color: rgba(255, 255, 255, 1.0); 184 | cursor: pointer; 185 | } 186 | 187 | svg:focus { 188 | color: rgba(255, 0, 0, 1.0); 189 | } 190 | 191 | .log { 192 | border: 1px solid rgba(255, 255, 255, 0.2); 193 | min-height: 460px; 194 | font-family: 'Courier New', Courier, monospace; 195 | font-size: 16px; 196 | padding: 1em; 197 | color: rgba(255, 255, 255, 1.0); 198 | text-align: left; 199 | background-color: rgba(0, 0, 0, 1.0); 200 | } 201 | 202 | .column p { 203 | font-weight: 300; 204 | font-size: 2em; 205 | padding: 0 0 0.5em; 206 | margin: 0; 207 | line-height: 1.5; 208 | } 209 | 210 | .ros-demos a, 211 | button { 212 | border: none; 213 | padding: 0.6em 1.2em; 214 | background: transparent; 215 | color: #fff; 216 | font-family: 'Lato', Calibri, Arial, sans-serif; 217 | cursor: pointer; 218 | display: inline-block; 219 | font-weight: 400; 220 | text-shadow: #000 1px 1px; 221 | } 222 | 223 | button { 224 | position: absolute; 225 | left: 32px; 226 | top: 36px; 227 | } 228 | 229 | .ros-demos a:hover, 230 | .ros-demos a:active, 231 | .ros-demos a.current-demo { 232 | background: rgba(216, 49, 91, 1); 233 | } 234 | 235 | button { 236 | background: transparent; 237 | } 238 | 239 | button:hover, 240 | button:active { 241 | border: 1px solid #fff; 242 | } 243 | 244 | .related { 245 | text-align: center; 246 | font-size: 1.5em; 247 | margin-top: 3em; 248 | clear: both; 249 | padding: 3em 0; 250 | } 251 | 252 | .related a { 253 | font-weight: 700; 254 | font-size: 0.9em; 255 | } 256 | 257 | .flex-container { 258 | display: flex; 259 | justify-content: center; 260 | flex-direction: row; 261 | text-align: center; 262 | } 263 | 264 | .flex-container .n { 265 | width: 180px; 266 | height: 100px; 267 | } 268 | 269 | .ltr { 270 | text-align: center; 271 | margin: 0 auto; 272 | display: flex; 273 | justify-content: center; 274 | flex-direction: column; 275 | align-items: center; 276 | } 277 | 278 | .utd { 279 | text-align: center; 280 | display: flex; 281 | justify-content: center; 282 | flex-direction: row; 283 | align-items: center; 284 | } 285 | 286 | .svg-arrow { 287 | display: flex; 288 | justify-content: center; 289 | flex-direction: row; 290 | text-align: center; 291 | } 292 | 293 | .svg-arrow svg { 294 | height: 20px; 295 | width: 20px; 296 | } 297 | 298 | .italic { 299 | font-style: italic; 300 | font-size: 20px; 301 | } 302 | 303 | .vno { 304 | visibility: hidden; 305 | } 306 | 307 | .arrow svg { 308 | color: rgba(255, 255, 255, 0.9); 309 | } 310 | 311 | .arrow svg:hover { 312 | color: rgba(255, 255, 255, 1.0); 313 | } 314 | 315 | .roslibjsworkflow .round { 316 | width: 180px; 317 | height: 100px; 318 | border-radius: 10px; 319 | overflow: hidden; 320 | justify-content: center; 321 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); 322 | } 323 | 324 | .roslibjsworkflow .round .mark { 325 | position: relative; 326 | left: 14px; 327 | top: -80px; 328 | width: 10px; 329 | height: 60px; 330 | border-radius: 12px; 331 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 332 | background: rgba(255, 255, 255, 0.5); 333 | z-index: 1000; 334 | } 335 | 336 | .roslibjsworkflow .round .roundnow { 337 | width: 100%; 338 | height: 100%; 339 | display: flex; 340 | justify-content: center; 341 | flex-direction: column; 342 | text-align: center; 343 | background: rgba(0, 0, 0, 0.4); 344 | -webkit-animation: roundnow 300s linear infinite; 345 | animation: roundnow 300s linear infinite; 346 | -webkit-transform: rotate(0deg); 347 | transform: rotate(0deg); 348 | font-size: 26px; 349 | } 350 | 351 | /* .roundnow:hover { 352 | background-color: rgba(10, 36, 99, 1.0); 353 | } */ 354 | 355 | .roundcolor { 356 | background: rgba(216, 49, 91, 1.0) !important; 357 | } 358 | 359 | .roundcolor2 { 360 | background: rgba(10, 36, 99, 1.0) !important; 361 | } 362 | 363 | input { 364 | border: 0px; 365 | padding: 16px 30px; 366 | margin: 2em auto; 367 | font-size: 1em; 368 | background-color: #fff; 369 | color: rgba(10, 36, 99, 1.0) ; 370 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); 371 | border-radius: 10px; 372 | width: 100%; 373 | } 374 | 375 | input:hover { 376 | cursor: pointer; 377 | color: #fff; 378 | background-color: rgba(10, 36, 99, 1.0); 379 | border: 0px; 380 | } 381 | 382 | 383 | @media screen and (max-width: 46.0625em) { 384 | .ros-header { 385 | margin-bottom: 0; 386 | padding-bottom: 1em; 387 | } 388 | .column { 389 | width: 100%; 390 | min-width: auto; 391 | min-height: 0; 392 | padding: 2em; 393 | text-align: center; 394 | } 395 | .column p { 396 | font-size: 1.5em; 397 | } 398 | .column:nth-child(2) { 399 | text-align: center; 400 | box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.1); 401 | } 402 | } 403 | 404 | @media screen and (max-width: 25em) { 405 | .ros-header { 406 | font-size: 80%; 407 | } 408 | .ros-top { 409 | font-size: 120%; 410 | } 411 | .ros-icon span { 412 | display: none; 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ROS 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 32 |
33 |
34 |
35 |
36 |
37 | 41 |
42 |
43 | 49 |
50 |
51 |
52 |
53 | 57 |
58 |
59 |
60 |
61 |
62 |
63 | Play 64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /demo/js/btnstate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ButtonState() { 4 | 5 | this.defaultColor = 0xfffff; 6 | this.btnClicked = false; 7 | this.svgClicked = false; 8 | } 9 | 10 | ButtonState.prototype.clearState = function() { 11 | console.log('clear button state'); 12 | let btns = document.getElementsByTagName('svg'); 13 | 14 | for (let i = 0; i < 4; i++) { 15 | btns[i].style.color = ''; 16 | } 17 | 18 | this.btnClicked = false; 19 | this.svgClicked = false; 20 | }; 21 | 22 | ButtonState.prototype.setSvgButton = function(btnId) { 23 | this.clearState(); 24 | 25 | console.log('setSVgButton'); 26 | let btnIndexMap = { 27 | up: 0, 28 | left: 1, 29 | right: 2, 30 | down: 3 31 | }; 32 | 33 | let btns = document.getElementsByTagName('svg'); 34 | btns[btnIndexMap[btnId]].style.color = 'green'; 35 | this.svgClicked = true; 36 | }; 37 | 38 | ButtonState.prototype.setStartButton = function(start) { 39 | this.clearState(); 40 | 41 | let btn = document.getElementById('start'); 42 | if (start) { 43 | btn.style.backgroundColor = 'green'; 44 | } else { 45 | btn.style.backgroundColor = ''; 46 | } 47 | }; 48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/js/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function WebRosController(ros, ros2dmap) { 4 | this.ros = ros; 5 | 6 | this.currentVelocity = { 7 | linear: {x: 0.0, y: 0.0, z: 0.0}, 8 | angular: {x: 0.0, y: 0.0, z: 0.0} 9 | }; 10 | this.zeroVelocity = { 11 | linear: {x: 0.0, y: 0.0, z: 0.0}, 12 | angular: {x: 0.0, y: 0.0, z: 0.0} 13 | }; 14 | this.defaultVelocity = { 15 | linear: {x: 0.10, y: 0.0, z: 0.0}, 16 | angular: {x: 0.0, y: 0.0, z: 0.0} 17 | }; 18 | 19 | this.pubVelTopicTimer = null; 20 | this.pubVelTopicInterval = 10; 21 | this.isRobotMoving = false; 22 | this.velocityTopic = new ROSLIB.Topic({ 23 | ros: this.ros, 24 | name: '/cmd_vel', 25 | messageType: 'geometry_msgs/Twist' 26 | }); 27 | 28 | this.poseX = 0; 29 | this.poseY = 0; 30 | this.radius = 0; 31 | this.rotation = 0; 32 | this.startTime = new Date(); 33 | this.endTime = new Date(); 34 | 35 | this.ros2dmap = ros2dmap; 36 | this.logger = new Logger('log'); 37 | } 38 | 39 | WebRosController.prototype.sendVelTopic = function(vel) { 40 | console.log('send velocity topic'); 41 | 42 | this.shutdownTimer(); 43 | this.pubVelTopicTimer = setInterval(() => { 44 | 45 | this.velocityTopic.publish(vel); 46 | this.updateMap(vel); 47 | this.logger.showTerminalLog(vel); 48 | 49 | this.startTime = new Date(); 50 | }, this.pubVelTopicInterval); 51 | }; 52 | 53 | WebRosController.prototype.updateMap = function(vel) { 54 | this.endTime = new Date(); 55 | 56 | let dt = (this.endTime - this.startTime) / 1000; 57 | 58 | let vx = vel.linear.x; 59 | let az = vel.angular.z; 60 | 61 | if (vx || az) { 62 | console.log('dt: ', dt); 63 | console.log('vx, az:', vx, ',', az); 64 | } 65 | let deltaX = (vx * Math.cos(this.radius)) * dt; 66 | let deltaY = (vx * Math.sin(this.radius)) * dt; 67 | let deltaRadius = az * dt; 68 | 69 | console.log(deltaX, deltaY, deltaRadius); 70 | 71 | if (deltaX || deltaY) { 72 | console.log('deltaX, deltaY:', deltaX, deltaY); 73 | this.poseX += deltaX; 74 | this.poseY += deltaY; 75 | } 76 | 77 | if (vx > 0) { 78 | this.rotation = this.radius * 180 / Math.PI; 79 | } 80 | 81 | if (az) { 82 | this.radius -= deltaRadius; 83 | this.rotation = this.radius * 180 / Math.PI; 84 | } 85 | 86 | 87 | if (deltaRadius > 0) { 88 | console.log('radius: ', this.radius); 89 | } 90 | 91 | if (vx || az) { 92 | console.log(this.poseX, ',', this.poseY, ',', this.rotation); 93 | } 94 | 95 | this.ros2dmap.update({ 96 | y: this.poseX, x: this.poseY 97 | }, this.rotation - 90); 98 | }; 99 | 100 | WebRosController.prototype.shutdownTimer = function() { 101 | if (this.pubVelTopicTimer) { 102 | clearInterval(this.pubVelTopicTimer); 103 | this.pubVelTopicTimer = null; 104 | } 105 | }; 106 | 107 | WebRosController.prototype.moveForward = function() { 108 | console.log('web ros controller: move forward'); 109 | console.log(this.currentVelocity); 110 | console.log(this.defaultVelocity); 111 | 112 | this.startTime = new Date(); 113 | if (this.currentVelocity.linear.x > 0) { 114 | this.sendVelTopic(this.currentVelocity); 115 | } else { 116 | this.sendVelTopic(this.defaultVelocity); 117 | } 118 | this.isRobotMoving = true; 119 | }; 120 | 121 | WebRosController.prototype.turnLeft = function() { 122 | console.log('web ros controller: turn left'); 123 | 124 | let turnLeftMsg = { 125 | linear: {x: 0.0, y: 0.0, z: 0.0}, 126 | angular: {x: 0.0, y: 0.0, z: Math.PI / 6} 127 | }; 128 | 129 | this.shutdownTimer(); 130 | this.startTime = new Date(); 131 | 132 | this.sendVelTopic(turnLeftMsg); 133 | }; 134 | 135 | WebRosController.prototype.turnRight = function() { 136 | console.log('web ros controller: turn left'); 137 | 138 | let turnRightMsg = { 139 | linear: {x: 0.0, y: 0.0, z: 0.0}, 140 | angular: {x: 0.0, y: 0.0, z: -Math.PI / 6} 141 | }; 142 | this.shutdownTimer(); 143 | this.startTime = new Date(); 144 | 145 | this.sendVelTopic(turnRightMsg); 146 | }; 147 | 148 | WebRosController.prototype.moveBack = function() { 149 | console.log('web ros controller: move back'); 150 | 151 | console.log(this.currentVelocity); 152 | console.log(this.defaultVelocity); 153 | 154 | this.startTime = new Date(); 155 | if (this.currentVelocity.linear.x) { 156 | if (this.currentVelocity.linear.x > 0) { 157 | this.currentVelocity.linear.x = -this.currentVelocity.linear.x; 158 | } 159 | this.sendVelTopic(this.currentVelocity); 160 | } else { 161 | let backVel = { 162 | linear: {x: 0.0, y: 0.0, z: 0.0}, 163 | angular: {x: 0.0, y: 0.0, z: 0.0} 164 | }; 165 | backVel.linear.x = -this.defaultVelocity.linear.x; 166 | this.sendVelTopic(backVel); 167 | } 168 | this.isRobotMoving = true; 169 | }; 170 | 171 | WebRosController.prototype.start = function() { 172 | console.log('web ros controller: start'); 173 | 174 | if (this.currentVelocity.linear.x) { 175 | this.sendVelTopic(this.currentVelocity); 176 | } else { 177 | this.currentVelocity.linear.x = -this.defaultVelocity.linear.x; 178 | this.sendVelTopic(this.defaultVelocity); 179 | } 180 | 181 | this.isRobotMoving = true; 182 | }; 183 | 184 | WebRosController.prototype.stop = function() { 185 | console.log('web ros controller: stop'); 186 | 187 | this.sendVelTopic(this.zeroVelocity); 188 | this.isRobotMoving = false; 189 | }; 190 | -------------------------------------------------------------------------------- /demo/js/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Logger(logDivId) { 4 | this.logDivId = logDivId; 5 | this.lineNum = 40; 6 | 7 | this.showLogHead = true; 8 | this.logArray = [ 9 | '[ros2-web-bridge:Bridge] Web bridge 70325038-136b-4ecd-a633-f617676f1ff9 is created', 10 | '[ros2-web-bridge:Bridge] JSON command received: {"op":"subscribe","id":"subscribe:/map:1",' + 11 | '"type":"nav_msgs/OccupancyGrid","topic":"/map","compression":"png", ' + 12 | '"throttle_rate":0,"queue_length":0}', 13 | '[ros2-web-bridge:Bridge] subscribe a topic named /map', 14 | '[rclnodejs:node] Finish creating subscription, topic = /map.', 15 | '[ros2-web-bridge:SubscriptionManager] Subscription has been created, and the topic name is /map.', 16 | '[ros2-web-bridge:Bridge] Response: {"op":"set_level","level":"none"}', 17 | '[ros2-web-bridge:Bridge] JSON command received: {"op":"advertise","id":"advertise:/cmd_vel:2",' + 18 | '"type":"geometry_msgs/Twist","topic":"/cmd_vel","latch":false,"queue_size":100}', 19 | '[ros2-web-bridge:Bridge] advertise a topic: /cmd_vel', 20 | '[rclnodejs:node] Finish creating publisher, topic = /cmd_vel.', 21 | '[ros2-web-bridge:ResourceProvider] Publisher has been created, and the topic name is /cmd_vel.', 22 | '[ros2-web-bridge:Bridge] Response: {"op":"set_level","level":"none"}' 23 | ]; 24 | this.count = 2; 25 | this.msgArrayCount = 0; 26 | }; 27 | 28 | Logger.prototype.showTerminalLog = function(msg) { 29 | 30 | let msgStr = JSON.stringify(msg); 31 | let logMsgArray = [ 32 | `[roslibjs] Publish sensor_msgs/Twist: ${msgStr}`, 33 | '[ros2-web-bridge:Bridge] JSON command received: ' + 34 | `{"op":"publish","id":"publish:/cmd_vel: ${this.count}","topic":"/cmd_vel", "msg": ${msgStr},"latch":false}`, 35 | `[ros2-web-bridge:Bridge] Publish a topic named /cmd_vel with ${msgStr}`, 36 | '[rclnodejs:publisher] Message of topic /cmd_vel has been published', 37 | '[ros2-web-bridge:Bridge] Response: {"op":"set_level","level":"none"}' 38 | ]; 39 | 40 | if (msg.linear.x || msg.angular.z) { 41 | if (this.showLogHead) { 42 | for (let i = 0; i < logMsgArray.length; i++) { 43 | this.logArray.push(logMsgArray[i]); 44 | } 45 | this.showLogHead = false; 46 | } else { 47 | let popElement = logMsgArray[this.msgArrayCount % 5]; 48 | this.msgArrayCount++; 49 | 50 | this.logArray.shift(); 51 | this.logArray.push(popElement); 52 | this.count++; 53 | } 54 | } 55 | 56 | document.getElementById(this.logDivId).innerHTML = this.logArray.join('
'); 57 | }; 58 | -------------------------------------------------------------------------------- /demo/js/ros2dmap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Ros2dMap(ros, options) { 4 | 5 | let divName = options.divName || 'ros2dmap'; 6 | let width = options.width || 400; 7 | let height = options.height || 400; 8 | 9 | let oldPose = {x: 0.0, y: 0.0}; 10 | let center = {x: 0.5, y: 0.5}; 11 | 12 | // The ROS2D.Viewer is a 2D scene manager with additional ROS functionality. 13 | let viewer2D = new ROS2D.Viewer({ 14 | divID: divName, 15 | width: width, 16 | height: height, 17 | background: '#99ffff' 18 | }); 19 | 20 | this.gridClient = new ROS2D.OccupancyGridClient({ 21 | ros: ros, 22 | rootObject: viewer2D.scene 23 | }); 24 | 25 | let grid = new ROS2D.Grid({ 26 | size: 100, 27 | cellSize: 0.10 28 | }); 29 | this.gridClient.rootObject.addChild(grid); 30 | 31 | let robotMaker = new ROS2D.NavigationArrow({ 32 | size: 4, 33 | strokeSize: 0.5, 34 | fillColor: createjs.Graphics.getRGB(0xcc, 0, 0xff, 0.62), 35 | pulse: false 36 | }); 37 | this.gridClient.rootObject.addChild(robotMaker); 38 | 39 | viewer2D.scaleToDimensions(1, 1); 40 | viewer2D.shift(-center.x, -center.y); 41 | 42 | robotMaker.x = 0; 43 | robotMaker.y = 0; 44 | robotMaker.scaleX = 0.01; 45 | robotMaker.scaleY = 0.01; 46 | robotMaker.rotation = -90; 47 | robotMaker.visible = true; 48 | 49 | this.update = function(pose, rotation) { 50 | robotMaker.x = pose.x; 51 | robotMaker.y = -pose.y; 52 | 53 | // robotMaker.rotation = viewer2D.scene.rosQuaternionToGlobalTheta(orientation); 54 | robotMaker.rotation = rotation; 55 | if (oldPose.x !== pose.x || oldPose.y !== pose.y) { 56 | viewer2D.shift(-oldPose.x + pose.x, -oldPose.y + pose.y); 57 | oldPose = pose; 58 | } 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /examples/html/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 59 | 60 | 61 | 62 |

Simple Clinet Example

63 |

This example will create a client to request a service named "add_two_ints" to calculate the sum of two values.

64 |
65 |

66 | Connecting to rosbridge... 67 |

68 | 71 | 74 | 77 |
78 |

79 |

80 | 1 + 2 = 81 | 82 |
83 |

84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/html/publisher.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 55 | 56 | 57 | 58 |

Simple Publisher Example

59 |

This example will pubilish a topic named "example_topic".

60 |
61 |

62 | Connecting to rosbridge... 63 |

64 | 67 | 70 | 73 |
74 |
75 |

76 | Publish message: 77 | 78 |

79 |
80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/html/service.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 56 | 57 | 58 | 59 |

Simple Service Example

60 |

This example will create a service named "add_two_ints" to calculate the sum of two values.

61 |
62 |

63 | Connecting to rosbridge... 64 |

65 | 68 | 71 | 74 |
75 |
76 | 77 |
78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/html/subscription.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 54 | 55 | 56 | 57 |

Simple Subscription Example

58 |

This example will subscribe a topic named "example_topic".

59 |
60 |

61 | Connecting to rosbridge... 62 |

63 | 66 | 69 | 72 |
73 |
74 | Receive message: 75 | 76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const express = require('express'); 18 | const app = express(); 19 | 20 | app.use(express.static('.')); 21 | 22 | app.listen(3000); 23 | console.log('The web server started on http://localhost:3000'); 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const rclnodejs = require('rclnodejs'); 18 | const WebSocket = require('ws'); 19 | const Bridge = require('./lib/bridge.js'); 20 | const debug = require('debug')('ros2-web-bridge:index'); 21 | 22 | // rclnodejs node 23 | let node; 24 | 25 | // Websocket server (or client if client mode set via --address) 26 | let server; 27 | let connectionAttempts = 0; 28 | 29 | // Map of bridge IDs to Bridge objects 30 | let bridgeMap = new Map(); 31 | 32 | function closeAllBridges() { 33 | bridgeMap.forEach((bridge, bridgeId) => { 34 | bridge.close(); 35 | }); 36 | } 37 | 38 | function shutDown(error) { 39 | // Closing the server triggers the individual connections to be closed. 40 | if (server) { 41 | server.close(); 42 | } 43 | if (!rclnodejs.isShutdown()) { 44 | rclnodejs.shutdown(); 45 | } 46 | if (error) { 47 | throw error; 48 | } 49 | } 50 | 51 | function createServer(options) { 52 | options = options || {}; 53 | options.address = options.address || null; 54 | process.on('exit', () => { 55 | debug('Application will exit.'); 56 | shutDown(); 57 | }); 58 | return rclnodejs.init() 59 | .then(() => { 60 | node = rclnodejs.createNode('ros2_web_bridge'); 61 | debug('ROS2 node started'); 62 | let timeout = options.delay_between_messages; 63 | if (timeout == undefined) { 64 | timeout = 0; 65 | } 66 | createConnection(options); 67 | spin(node, timeout); 68 | }) 69 | .catch(error => shutDown(error)); 70 | } 71 | 72 | function spin(node, timeout) { 73 | if (rclnodejs.isShutdown()) { 74 | shutDown(); 75 | return; 76 | } 77 | node.spinOnce(); 78 | setTimeout(spin, timeout, node, timeout); 79 | } 80 | 81 | function createConnection(options) { 82 | if (options.address != null) { 83 | debug('Starting in client mode; connecting to ' + options.address); 84 | server = new WebSocket(options.address); 85 | } else { 86 | options.port = options.port || 9090; 87 | debug('Starting server on port ' + options.port); 88 | server = new WebSocket.Server({port: options.port}); 89 | } 90 | 91 | const makeBridge = (ws) => { 92 | let bridge = new Bridge(node, ws, options.status_level); 93 | bridgeMap.set(bridge.bridgeId, bridge); 94 | 95 | bridge.on('error', (error) => { 96 | debug(`Bridge ${bridge.bridgeId} closing with error: ${error}`); 97 | bridge.close(); 98 | bridgeMap.delete(bridge.bridgeId); 99 | }); 100 | 101 | bridge.on('close', (bridgeId) => { 102 | bridgeMap.delete(bridgeId); 103 | }); 104 | }; 105 | 106 | server.on('open', () => { 107 | debug('Connected as client'); 108 | connectionAttempts = 0; 109 | }); 110 | 111 | if (options.address) { 112 | makeBridge(server); 113 | } else { 114 | server.on('connection', makeBridge); 115 | } 116 | 117 | server.on('error', (error) => { 118 | closeAllBridges(); 119 | debug(`WebSocket error: ${error}`); 120 | }); 121 | 122 | server.on('close', (event) => { 123 | debug(`Websocket closed: ${event}`); 124 | if (options.address) { 125 | closeAllBridges(); 126 | connectionAttempts++; 127 | // Gradually increase reconnection interval to prevent 128 | // overwhelming the server, up to a maximum delay of ~1 minute 129 | // https://en.wikipedia.org/wiki/Exponential_backoff 130 | const delay = Math.pow(1.5, Math.min(10, Math.floor(Math.random() * connectionAttempts))); 131 | debug(`Reconnecting to ${options.address} in ${delay.toFixed(2)} seconds`); 132 | setTimeout(() => createConnection(options), delay*1000); 133 | } 134 | }); 135 | 136 | let wsAddr = options.address || `ws://localhost:${options.port}`; 137 | console.log(`Websocket started on ${wsAddr}`); 138 | // gracefuly shutdown rosbridge node commanding terminal 139 | process.on('SIGINT', () => process.exit(1)); 140 | } 141 | 142 | module.exports = { 143 | createServer: createServer, 144 | }; 145 | -------------------------------------------------------------------------------- /lib/bridge.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const ResourceProvider = require('./resource_provider.js'); 18 | const debug = require('debug')('ros2-web-bridge:Bridge'); 19 | const EventEmitter = require('events'); 20 | const uuidv4 = require('uuid/v4'); 21 | const {validator} = require('rclnodejs'); 22 | 23 | const STATUS_LEVELS = ['error', 'warning', 'info', 'none']; 24 | 25 | class MessageParser { 26 | constructor() { 27 | this._buffer = ''; 28 | } 29 | 30 | process(message) { 31 | // The logic below is translated from the current implementation of rosbridge_suit, 32 | // see https://github.com/RobotWebTools/rosbridge_suite/blob/develop/rosbridge_library/src/rosbridge_library/protocol.py 33 | this._buffer += message; 34 | let msg = null; 35 | try { 36 | msg = JSON.parse(this._buffer); 37 | this._buffer = ''; 38 | } 39 | catch (e) { 40 | if (e instanceof SyntaxError) { 41 | let openingBrackets = this._buffer.indexOf('{'); 42 | let closingBrackets = this._buffer.indexOf('}'); 43 | 44 | for (let start = 0; start <= openingBrackets; start++) { 45 | for (let end = 0; end <= closingBrackets; end++) { 46 | try { 47 | msg = JSON.parse(this._buffer.substring(start, end + 1)); 48 | if (msg.op) { 49 | self._buffer = self._buffer.substr(end + 1, this._buffer.length); 50 | break; 51 | } 52 | } 53 | catch (e) { 54 | if (e instanceof SyntaxError) { 55 | continue; 56 | } 57 | } 58 | } 59 | if (msg) { 60 | break; 61 | } 62 | } 63 | } 64 | } 65 | return msg; 66 | } 67 | } 68 | 69 | class Bridge extends EventEmitter { 70 | 71 | constructor(node, ws, statusLevel) { 72 | super(); 73 | this._ws = ws; 74 | this._parser = new MessageParser(); 75 | this._bridgeId = this._generateRandomId(); 76 | this._servicesResponse = new Map(); 77 | this._closed = false; 78 | this._resourceProvider = new ResourceProvider(node, this._bridgeId); 79 | this._registerConnectionEvent(ws); 80 | this._rebuildOpMap(); 81 | this._topicsPublished = new Map(); 82 | this._setStatusLevel(statusLevel || 'error'); 83 | debug(`Web bridge ${this._bridgeId} is created`); 84 | } 85 | 86 | _registerConnectionEvent(ws) { 87 | ws.on('message', (message) => { 88 | this._receiveMessage(message); 89 | }); 90 | 91 | ws.on('close', () => { 92 | this.close(); 93 | this.emit('close', this._bridgeId); 94 | debug(`Web bridge ${this._bridgeId} is closed`); 95 | }); 96 | 97 | ws.on('error', (error) => { 98 | this.emit('error', error); 99 | debug(`Web socket of bridge ${this._bridgeId} error: ${error}`); 100 | }); 101 | } 102 | 103 | close() { 104 | if (!this._closed) { 105 | this._resourceProvider.clean(); 106 | this._servicesResponse.clear(); 107 | this._topicsPublished.clear(); 108 | this._closed = true; 109 | } 110 | } 111 | 112 | _generateRandomId() { 113 | return uuidv4(); 114 | } 115 | 116 | _exractMessageType(type) { 117 | if (type.indexOf('/msg/') === -1) { 118 | const splitted = type.split('/'); 119 | return splitted[0] + '/msg/' + splitted[1]; 120 | } 121 | return type; 122 | } 123 | 124 | _exractServiceType(type) { 125 | if (type.indexOf('/srv/') === -1) { 126 | const splitted = type.split('/'); 127 | return splitted[0] + '/srv/' + splitted[1]; 128 | } 129 | return type; 130 | } 131 | 132 | _receiveMessage(message) { 133 | const command = this._parser.process(message); 134 | if (!command) return; 135 | 136 | debug(`JSON command received: ${JSON.stringify(command)}`); 137 | this.executeCommand(command); 138 | } 139 | 140 | get bridgeId() { 141 | return this._bridgeId; 142 | } 143 | 144 | get closed() { 145 | return this._closed; 146 | } 147 | 148 | _registerOpMap(opCode, callback) { 149 | this._opMap = this._opMap || {}; 150 | 151 | if (this._opMap[opCode]) { 152 | debug(`Warning: existing callback of '${opCode}'' will be overwritten by new callback`); 153 | } 154 | this._opMap[opCode] = callback; 155 | } 156 | 157 | _rebuildOpMap() { 158 | this._registerOpMap('set_level', (command) => { 159 | if (STATUS_LEVELS.indexOf(command.level) === -1) { 160 | throw new Error(`Invalid status level ${command.level}; must be one of ${STATUS_LEVELS}`); 161 | } 162 | this._setStatusLevel(command.level); 163 | }); 164 | 165 | this._registerOpMap('advertise', (command) => { 166 | let topic = command.topic; 167 | if (this._topicsPublished.has(topic) && (this._topicsPublished.get(topic) !== command.type)) { 168 | throw new Error(`The topic ${topic} already exists with a different type ${this._topicsPublished.get(topic)}.`); 169 | } 170 | debug(`advertise a topic: ${topic}`); 171 | this._topicsPublished.set(topic, command.type); 172 | this._resourceProvider.createPublisher(this._exractMessageType(command.type), topic); 173 | }); 174 | 175 | this._registerOpMap('unadvertise', (command) => { 176 | let topic = command.topic; 177 | this._validateTopicOrService(command.topic); 178 | 179 | if (!this._topicsPublished.has(topic)) { 180 | let error = new Error(`The topic ${topic} does not exist`); 181 | error.level = 'warning'; 182 | throw error; 183 | } 184 | debug(`unadvertise a topic: ${topic}`); 185 | this._topicsPublished.delete(topic); 186 | this._resourceProvider.destroyPublisher(topic); 187 | }); 188 | 189 | this._registerOpMap('publish', (command) => { 190 | debug(`Publish a topic named ${command.topic} with ${JSON.stringify(command.msg)}`); 191 | 192 | if (!this._topicsPublished.has(command.topic)) { 193 | let error = new Error(`The topic ${command.topic} does not exist`); 194 | error.level = 'error'; 195 | throw error; 196 | } 197 | let publisher = this._resourceProvider.getPublisherByTopicName(command.topic); 198 | if (publisher) { 199 | publisher.publish(command.msg); 200 | } 201 | }); 202 | 203 | this._registerOpMap('subscribe', (command) => { 204 | debug(`subscribe a topic named ${command.topic}`); 205 | 206 | this._resourceProvider.createSubscription(this._exractMessageType(command.type), 207 | command.topic, 208 | this._sendSubscriptionResponse.bind(this)); 209 | }); 210 | 211 | this._registerOpMap('unsubscribe', (command) => { 212 | let topic = command.topic; 213 | this._validateTopicOrService(topic); 214 | 215 | if (!this._resourceProvider.hasSubscription(topic)) { 216 | let error = new Error(`The topic ${topic} does not exist.`); 217 | error.level = 'warning'; 218 | throw error; 219 | } 220 | debug(`unsubscribe a topic named ${topic}`); 221 | this._resourceProvider.destroySubscription(command.topic); 222 | }); 223 | 224 | this._registerOpMap('call_service', (command) => { 225 | let serviceName = command.service; 226 | let client = 227 | this._resourceProvider.createClient(this._exractServiceType(command.type), serviceName); 228 | 229 | if (client) { 230 | client.sendRequest(command.args, (response) => { 231 | let serviceResponse = 232 | {op: 'service_response', service: command.service, values: response, id: command.id, result: true}; 233 | 234 | this._ws.send(JSON.stringify(serviceResponse)); 235 | }); 236 | } 237 | }); 238 | 239 | this._registerOpMap('advertise_service', (command) => { 240 | let serviceName = command.service; 241 | let service = this._resourceProvider.createService( 242 | this._exractServiceType(command.type), 243 | serviceName, 244 | (request, response) => { 245 | let id = this._generateRandomId(); 246 | let serviceRequest = {op: 'call_service', service: command.service, args: request, id: id}; 247 | this._servicesResponse.set(id, response); 248 | this._ws.send(JSON.stringify(serviceRequest)); 249 | }); 250 | }); 251 | 252 | this._registerOpMap('service_response', (command) => { 253 | let serviceName = command.service; 254 | let id = command.id; 255 | let response = this._servicesResponse.get(id); 256 | if (response) { 257 | response.send(command.values); 258 | this._servicesResponse.delete(id); 259 | } 260 | }); 261 | 262 | this._registerOpMap('unadvertise_service', (command) => { 263 | let serviceName = command.service; 264 | this._validateTopicOrService(serviceName); 265 | 266 | if (!this._resourceProvider.hasService(serviceName)) { 267 | let error = new Error(`The service ${serviceName} does not exist.`); 268 | error.level = 'warning'; 269 | throw error; 270 | } 271 | debug(`unadvertise a service: ${serviceName}`); 272 | this._resourceProvider.destroyService(command.service); 273 | }); 274 | } 275 | 276 | executeCommand(command) { 277 | try { 278 | const op = this._opMap[command.op]; 279 | if (!op) { 280 | throw new Error(`Operation ${command.op} is not supported`); 281 | } 282 | op.apply(this, [command]); 283 | this._sendBackOperationStatus(command.id, 'none', 'OK'); 284 | } catch (e) { 285 | e.id = command.id; 286 | e.op = command.op; 287 | this._sendBackErrorStatus(e); 288 | } 289 | } 290 | 291 | _sendSubscriptionResponse(topicName, message) { 292 | debug('Send message to subscription.'); 293 | let response = {op: 'publish', topic: topicName, msg: message}; 294 | this._ws.send(JSON.stringify(response)); 295 | } 296 | 297 | _sendBackErrorStatus(error) { 298 | const msg = `${error.op}: ${error}`; 299 | return this._sendBackOperationStatus(error.id, error.level || 'error', msg); 300 | } 301 | 302 | _sendBackOperationStatus(id, level, msg) { 303 | let command = { 304 | op: 'status', 305 | level: level || 'none', 306 | msg: msg || '', 307 | id: id, 308 | }; 309 | if (this._statusLevel < STATUS_LEVELS.indexOf(level)) { 310 | debug('Suppressed: ' + JSON.stringify(command)); 311 | return; 312 | } 313 | debug('Response: ' + JSON.stringify(command)); 314 | this._ws.send(JSON.stringify(command)); 315 | } 316 | 317 | _setStatusLevel(level) { 318 | this._statusLevel = STATUS_LEVELS.indexOf(level); 319 | debug(`Status level set to ${level} (${this._statusLevel})`); 320 | } 321 | 322 | _validateTopicOrService(name) { 323 | if (name.startsWith('/')) { 324 | validator.validateFullTopicName(name); 325 | } else { 326 | validator.validateTopicName(name); 327 | } 328 | } 329 | 330 | get ws() { 331 | return this._ws; 332 | } 333 | } 334 | 335 | module.exports = Bridge; 336 | -------------------------------------------------------------------------------- /lib/ref_counting_handle.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const debug = require('debug')('ros2-web-bridge:RefCountingHandle'); 18 | 19 | class RefCountingHandle { 20 | constructor(object, destroyHandle) { 21 | if (object) { 22 | this._object = object; 23 | this._count = 1; 24 | this._destroyHandle = destroyHandle; 25 | } 26 | } 27 | 28 | get() { 29 | return this._object; 30 | } 31 | 32 | release() { 33 | if (this._count > 0) { 34 | if (--this._count === 0) { 35 | this._destroyHandle(this._object); 36 | this._object = undefined; 37 | debug('Handle is destroyed.'); 38 | } 39 | } 40 | } 41 | 42 | retain() { 43 | this._count++; 44 | } 45 | 46 | destroy() { 47 | if (this._count > 0) { 48 | this._destroyHandle(this._object); 49 | this._count = 0; 50 | this._object = undefined; 51 | debug('Handle is destroyed.'); 52 | } 53 | } 54 | 55 | get count() { 56 | return this._count; 57 | } 58 | } 59 | 60 | module.exports = RefCountingHandle; 61 | -------------------------------------------------------------------------------- /lib/resource_provider.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const SubscriptionManager = require('./subscription_manager.js'); 18 | const RefCountingHandle = require('./ref_counting_handle.js'); 19 | const debug = require('debug')('ros2-web-bridge:ResourceProvider'); 20 | 21 | class ResourceProvider { 22 | constructor(node, bridgeId) { 23 | SubscriptionManager.init(node); 24 | this._bridgeId = bridgeId; 25 | this._node = node; 26 | this._publishers = new Map(); 27 | this._clients = new Map(); 28 | this._services = new Map(); 29 | } 30 | 31 | getPublisherByTopicName(topicName) { 32 | return this._publishers.get(topicName).get(); 33 | } 34 | 35 | getSubscriptionByTopicName(topicName) { 36 | return SubscriptionManager.getInstance().getSubscriptionByTopicName(topicName).get(); 37 | } 38 | 39 | getClientByServiceName(serviceName) { 40 | return this._clients.get(serviceName).get(); 41 | } 42 | 43 | getServiceByServiceName(serviceName) { 44 | return this._services.get(serviceName).get(); 45 | } 46 | 47 | createPublisher(messageType, topicName) { 48 | let handle = this._publishers.get(topicName); 49 | if (!handle) { 50 | handle = new RefCountingHandle(this._node.createPublisher(messageType, topicName), 51 | this._node.destroyPublisher.bind(this._node)); 52 | this._publishers.set(topicName, handle); 53 | debug(`Publisher has been created, and the topic name is ${topicName}.`); 54 | } else { 55 | handle.retain(); 56 | } 57 | return handle.get(); 58 | } 59 | 60 | createSubscription(messageType, topicName, callback) { 61 | return SubscriptionManager.getInstance().createSubscription(messageType, topicName, this._bridgeId, callback); 62 | } 63 | 64 | createClient(serviceType, serviceName) { 65 | let handle = this._clients.get(serviceName); 66 | if (!handle) { 67 | handle = new RefCountingHandle(this._node.createClient(serviceType, serviceName, {enableTypedArray: false}), 68 | this._node.destroyClient.bind(this._node)); 69 | this._clients.set(serviceName, handle); 70 | debug(`Client has been created, and the service name is ${serviceName}.`); 71 | } else { 72 | handle.retain(); 73 | } 74 | return handle.get(); 75 | } 76 | 77 | createService(serviceType, serviceName, callback) { 78 | let handle = this._services.get(serviceName); 79 | if (!handle) { 80 | handle = new RefCountingHandle(this._node.createService(serviceType, serviceName, {enableTypedArray: false}, 81 | (request, response) => { 82 | callback(request, response); 83 | }), this._node.destroyService.bind(this._node)); 84 | this._services.set(serviceName, handle); 85 | debug(`Service has been created, and the service name is ${serviceName}.`); 86 | } else { 87 | handle.retain(); 88 | } 89 | return handle.get(); 90 | } 91 | 92 | destroyPublisher(topicName) { 93 | if (this._publishers.has(topicName)) { 94 | let handle = this._publishers.get(topicName); 95 | handle.release(); 96 | this._removeInvalidHandle(this._publishers, handle, topicName); 97 | } 98 | } 99 | 100 | destroySubscription(topicName) { 101 | SubscriptionManager.getInstance().destroySubscription(topicName, this._bridgeId); 102 | } 103 | 104 | _destroySubscriptionForBridge() { 105 | SubscriptionManager.getInstance().destroyForBridgeId(this._bridgeId); 106 | } 107 | 108 | destroyClient(serviceName) { 109 | if (this._clients.has(serviceName)) { 110 | let handle = this._clients.get(serviceName); 111 | handle.release(); 112 | this._removeInvalidHandle(this._clients, handle, serviceName); 113 | } 114 | } 115 | 116 | destroyService(serviceName) { 117 | if (this._services.has(serviceName)) { 118 | let handle = this._services.get(serviceName); 119 | handle.release(); 120 | this._removeInvalidHandle(this._services, handle, serviceName); 121 | } 122 | } 123 | 124 | hasService(serviceName) { 125 | return this._services.has(serviceName); 126 | } 127 | 128 | hasSubscription(topicName) { 129 | return SubscriptionManager.getInstance().getSubscriptionByTopicName(topicName) !== undefined; 130 | } 131 | 132 | clean() { 133 | this._cleanHandleInMap(this._publishers); 134 | this._cleanHandleInMap(this._services); 135 | this._cleanHandleInMap(this._clients); 136 | this._destroySubscriptionForBridge(); 137 | } 138 | 139 | _removeInvalidHandle(map, handle, name) { 140 | if (handle.count === 0) { 141 | map.delete(name); 142 | } 143 | } 144 | 145 | _cleanHandleInMap(map) { 146 | map.forEach(handle => { 147 | handle.destroy(); 148 | }); 149 | map.clear(); 150 | } 151 | } 152 | 153 | module.exports = ResourceProvider; 154 | -------------------------------------------------------------------------------- /lib/rosauth.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | // Same authentication logic with https://github.com/GT-RAIL/rosauth/blob/develop/src/ros_mac_authentication.cpp 18 | 19 | const crypto = require('crypto'); 20 | 21 | let secretFile = ''; 22 | 23 | function sha512(text) { 24 | const hash = crypto.createHash('sha512'); 25 | hash.update(text); 26 | return hash.digest('hex'); 27 | } 28 | 29 | function getSecret() { 30 | const path = require('path'); 31 | const fs = require('fs'); 32 | const file = path.resolve(__dirname, secretFile); 33 | // eslint-disable-next-line 34 | const content = fs.readFileSync(file).toString(); 35 | getSecret = function() { return content; }; 36 | return content; 37 | } 38 | 39 | function gt(l, s) { 40 | return (l.sec == s.sec && l.nanosec > s.nanosec) || l.sec > s.sec; 41 | } 42 | 43 | const NANOSEC_IN_A_SEC = 1000 * 1000 * 1000; 44 | 45 | function diffTime(l, s) { 46 | let nanodiff = l.nanosec - s.nanosec; 47 | let secdiff = l.sec - s.sec; 48 | if (l.nanosec < s.nanosec) { 49 | nanodiff += NANOSEC_IN_A_SEC; 50 | secdiff += 1; 51 | } 52 | return secdiff + nanodiff / NANOSEC_IN_A_SEC; 53 | } 54 | 55 | function getJavaScriptTime() { 56 | const t = new Date().getTime(); 57 | return {sec: Math.floor(t / 1000), nanosec: (t % 1000) * 1000 * 1000}; 58 | } 59 | 60 | function authenticate(msg) { 61 | if (Number.isNaN(msg.t.sec) || Number.isNaN(msg.t.nanosec) || 62 | Number.isNaN(msg.end.sec) || Number.isNaN(msg.end.nanosec) || 63 | msg.t.sec < 0 || msg.end.sec < 0 || 64 | msg.t.nanosec >= NANOSEC_IN_A_SEC || msg.end.nanosec >= NANOSEC_IN_A_SEC || 65 | msg.t.nanosec < 0 || msg.end.nanosec < 0) { 66 | return false; 67 | } 68 | 69 | // We don't get time from ROS system 70 | // because it might not be a system-clock timestamp 71 | const t = getJavaScriptTime(); 72 | let diff; 73 | if (gt(msg.t, t)) { 74 | diff = diffTime(msg.t, t); 75 | } else { 76 | diff = diffTime(t, msg.t); 77 | } 78 | 79 | if (diff < 5 && gt(msg.end, t)) { 80 | const text = getSecret() + msg.client + msg.dest + msg.rand + msg.t.sec + msg.level + msg.end.sec; 81 | const hash = sha512(text); 82 | return msg.mac === hash; 83 | } 84 | 85 | return false; 86 | } 87 | 88 | function setSecretFile(file) { 89 | secretFile = file; 90 | } 91 | 92 | module.exports = { 93 | authenticate, 94 | setSecretFile, 95 | }; 96 | -------------------------------------------------------------------------------- /lib/subscription_manager.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const RefCountingHandle = require('./ref_counting_handle.js'); 18 | const debug = require('debug')('ros2-web-bridge:SubscriptionManager'); 19 | 20 | class HandleWithCallbacks extends RefCountingHandle { 21 | constructor(object, destroyHandle) { 22 | super(object, destroyHandle); 23 | this._callbacks = new Map(); 24 | } 25 | 26 | addCallback(id, callback) { 27 | this._callbacks.set(id, callback); 28 | } 29 | 30 | removeCallback(id) { 31 | this._callbacks.delete(id); 32 | } 33 | 34 | hasCallbackForId(id) { 35 | return this._callbacks.has(id); 36 | } 37 | 38 | get callbacks() { 39 | return Array.from(this._callbacks.values()); 40 | } 41 | } 42 | 43 | class SubscriptionManager { 44 | constructor(node) { 45 | this._subscripions = new Map(); 46 | this._node = node; 47 | } 48 | 49 | getSubscriptionByTopicName(topicName) { 50 | return this._subscripions.get(topicName); 51 | } 52 | 53 | createSubscription(messageType, topicName, bridgeId, callback) { 54 | let handle = this._subscripions.get(topicName); 55 | 56 | if (!handle) { 57 | let subscription = this._node.createSubscription(messageType, topicName, {enableTypedArray: false}, (message) => { 58 | this._subscripions.get(topicName).callbacks.forEach(callback => { 59 | callback(topicName, message); 60 | }); 61 | }); 62 | handle = new HandleWithCallbacks(subscription, this._node.destroySubscription.bind(this._node)); 63 | handle.addCallback(bridgeId, callback); 64 | this._subscripions.set(topicName, handle); 65 | debug(`Subscription has been created, and the topic name is ${topicName}.`); 66 | 67 | return handle.get(); 68 | } 69 | 70 | handle.addCallback(bridgeId, callback); 71 | handle.retain(); 72 | return handle.get(); 73 | } 74 | 75 | destroySubscription(topicName, bridgeId) { 76 | if (this._subscripions.has(topicName)) { 77 | let handle = this._subscripions.get(topicName); 78 | if (handle.hasCallbackForId(bridgeId)) { 79 | handle.removeCallback(bridgeId); 80 | handle.release(); 81 | if (handle.count === 0) { 82 | this._subscripions.delete(topicName); 83 | } 84 | } 85 | } 86 | } 87 | 88 | destroyForBridgeId(bridgeId) { 89 | this._subscripions.forEach(handle => { 90 | if (handle.hasCallbackForId(bridgeId)) { 91 | handle.removeCallback(bridgeId); 92 | handle.release(); 93 | this._removeInvalidHandle(); 94 | } 95 | }); 96 | } 97 | 98 | _removeInvalidHandle() { 99 | this._subscripions.forEach((handle, topicName, map) => { 100 | if (handle.count === 0) { 101 | map.delete(topicName); 102 | } 103 | }); 104 | } 105 | } 106 | 107 | let subscriptionManager = { 108 | _instance: undefined, 109 | 110 | init(node) { 111 | if (!this._instance) { 112 | this._instance = new SubscriptionManager(node); 113 | } 114 | }, 115 | 116 | getInstance() { 117 | return this._instance; 118 | } 119 | }; 120 | 121 | module.exports = subscriptionManager; 122 | -------------------------------------------------------------------------------- /npm-pack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2018 Intel Corporation. All rights reserved. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | #Usage: npm-pack.sh 18 | 19 | WORKDIR=`mktemp -d` 20 | RAWMODULEDIR="ros2-web-bridge/" 21 | MODULEDIR="$WORKDIR/$RAWMODULEDIR" 22 | 23 | mkdir -p $MODULEDIR 24 | rsync -a . $MODULEDIR --exclude dist --exclude node_modules --exclude Dockerfile --exclude appveyor.yml --exclude .travis.yml 25 | 26 | pushd . > /dev/null 27 | cd $WORKDIR 28 | FILENAME=`npm pack $RAWMODULEDIR` 29 | TARFILENAME="$WORKDIR/$FILENAME" 30 | 31 | popd > /dev/null 32 | mkdir -p dist 33 | cp -f $TARFILENAME ./dist/ 34 | rm -rf $WORKDIR 35 | 36 | echo "Generated ./dist/$FILENAME" 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ros2-web-bridge", 3 | "version": "0.3.1", 4 | "description": "Bridge the web clients to ROS2.0 by a JSON interface", 5 | "main": "index.js", 6 | "keywords": [ 7 | "ros2", 8 | "webbridge", 9 | "rcl", 10 | "rclnodejs" 11 | ], 12 | "bin": { 13 | "rosbridge": "bin/rosbridge.js" 14 | }, 15 | "scripts": { 16 | "test": "mocha test/nodejs/", 17 | "wsserver": "node bin/rosbridge.js", 18 | "browser": "node test/browser/test-html.js", 19 | "lint": "eslint --max-warnings=0 index.js lib examples test", 20 | "protocol": "mocha test/nodejs/protocol/entry.js test/nodejs/protocol/entry-client-mode.js", 21 | "ci": "npm run test && npm run protocol && npm run browser" 22 | }, 23 | "license": "Apache-2.0", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/RobotWebTools/ros2-web-bridge.git" 27 | }, 28 | "authors": [ 29 | "Minggang Wang ", 30 | "Kenny Yuan ", 31 | "Wanming Lin ", 32 | "Zhong Qiu " 33 | ], 34 | "devDependencies": { 35 | "async": "^3.2.0", 36 | "express": "^4.16.2", 37 | "eslint": "^6.8.0", 38 | "js-sha512": "^0.8.0", 39 | "mocha": "^7.1.1", 40 | "jsdom": "^16.2.1", 41 | "puppeteer": "^2.1.1" 42 | }, 43 | "dependencies": { 44 | "commander": "^5.0.0", 45 | "debug": "^4.1.1", 46 | "rclnodejs": "^0.x.1", 47 | "uuid": "^7.0.2", 48 | "ws": "^7.1.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | This directory contains the files for two kinds of tests: 3 | * browser: tests that are running in the Google Chromium browser environment. `testharness.js` framework is used in these tests. 4 | * node.js: tests that are running in Node.js runtime and `mocha` framework is used in these tests. 5 | 6 | ## Run tests 7 | * For nodejs tests, run 8 | ``` 9 | npm run test 10 | ``` 11 | * For browser tests, run 12 | ``` 13 | npm run browser 14 | ``` 15 | -------------------------------------------------------------------------------- /test/browser/html_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "htmls": [ 3 | { 4 | "suite": "test-example.html", 5 | "waitingTime": 200 6 | }, 7 | { 8 | "suite": "test-goal.html", 9 | "waitingTime": 200 10 | }, 11 | { 12 | "suite": "test-topic.html", 13 | "waitingTime": 2000 14 | }, 15 | { 16 | "suite": "test-ros.html", 17 | "waitingTime": 3000 18 | }, 19 | { 20 | "suite": "test-service.html", 21 | "waitingTime": 2000 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test/browser/resources/LICENSE: -------------------------------------------------------------------------------- 1 | W3C 3-clause BSD License 2 | 3 | http://www.w3.org/Consortium/Legal/2008/03-bsd-license.html 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of works must retain the original copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the original copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the W3C nor the names of its contributors may be 17 | used to endorse or promote products derived from this work without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /test/browser/resources/testharness.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans; 3 | } 4 | 5 | #log .warning, 6 | #log .warning a { 7 | color: black; 8 | background: yellow; 9 | } 10 | 11 | #log .error, 12 | #log .error a { 13 | color: white; 14 | background: red; 15 | } 16 | 17 | section#summary { 18 | margin-bottom:1em; 19 | } 20 | 21 | table#results { 22 | border-collapse:collapse; 23 | table-layout:fixed; 24 | width:100%; 25 | } 26 | 27 | table#results th:first-child, 28 | table#results td:first-child { 29 | width:4em; 30 | } 31 | 32 | table#results th:last-child, 33 | table#results td:last-child { 34 | width:50%; 35 | } 36 | 37 | table#results.assertions th:last-child, 38 | table#results.assertions td:last-child { 39 | width:35%; 40 | } 41 | 42 | table#results th { 43 | padding:0; 44 | padding-bottom:0.5em; 45 | border-bottom:medium solid black; 46 | } 47 | 48 | table#results td { 49 | padding:1em; 50 | padding-bottom:0.5em; 51 | border-bottom:thin solid black; 52 | } 53 | 54 | tr.pass > td:first-child { 55 | color:green; 56 | } 57 | 58 | tr.fail > td:first-child { 59 | color:red; 60 | } 61 | 62 | tr.timeout > td:first-child { 63 | color:red; 64 | } 65 | 66 | tr.notrun > td:first-child { 67 | color:blue; 68 | } 69 | 70 | .pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child { 71 | font-variant:small-caps; 72 | } 73 | 74 | table#results span { 75 | display:block; 76 | } 77 | 78 | table#results span.expected { 79 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 80 | white-space:pre; 81 | } 82 | 83 | table#results span.actual { 84 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 85 | white-space:pre; 86 | } 87 | 88 | span.ok { 89 | color:green; 90 | } 91 | 92 | tr.error { 93 | color:red; 94 | } 95 | 96 | span.timeout { 97 | color:red; 98 | } 99 | 100 | span.ok, span.timeout, span.error { 101 | font-variant:small-caps; 102 | } -------------------------------------------------------------------------------- /test/browser/resources/testharnessreport.js: -------------------------------------------------------------------------------- 1 | /* global add_completion_callback */ 2 | /* global setup */ 3 | 4 | /* 5 | * This file is intended for vendors to implement code needed to integrate 6 | * testharness.js tests with their own test systems. 7 | * 8 | * Typically test system integration will attach callbacks when each test has 9 | * run, using add_result_callback(callback(test)), or when the whole test file 10 | * has completed, using 11 | * add_completion_callback(callback(tests, harness_status)). 12 | * 13 | * For more documentation about the callback functions and the 14 | * parameters they are called with see testharness.js 15 | */ 16 | 17 | function dump_test_results(tests, status) { 18 | var results_element = document.createElement("script"); 19 | results_element.type = "text/json"; 20 | results_element.id = "__testharness__results__"; 21 | var test_results = tests.map(function(x) { 22 | return {name:x.name, status:x.status, message:x.message, stack:x.stack} 23 | }); 24 | var data = {test:window.location.href, 25 | tests:test_results, 26 | status: status.status, 27 | message: status.message, 28 | stack: status.stack}; 29 | results_element.textContent = JSON.stringify(data); 30 | 31 | // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element' 32 | // is inserted at a location that results in a valid document. 33 | var parent = document.body 34 | ? document.body // is required in XHTML documents 35 | : document.documentElement; // fallback for optional in HTML5, SVG, etc. 36 | 37 | parent.appendChild(results_element); 38 | } 39 | 40 | add_completion_callback(dump_test_results); 41 | 42 | /* If the parent window has a testharness_properties object, 43 | * we use this to provide the test settings. This is used by the 44 | * default in-browser runner to configure the timeout and the 45 | * rendering of results 46 | */ 47 | try { 48 | if (window.opener && "testharness_properties" in window.opener) { 49 | /* If we pass the testharness_properties object as-is here without 50 | * JSON stringifying and reparsing it, IE fails & emits the message 51 | * "Could not complete the operation due to error 80700019". 52 | */ 53 | setup(JSON.parse(JSON.stringify(window.opener.testharness_properties))); 54 | } 55 | } catch (e) { 56 | } 57 | // vim: set expandtab shiftwidth=4 tabstop=4: 58 | -------------------------------------------------------------------------------- /test/browser/server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const express = require('express'); 18 | const app = express(); 19 | 20 | app.use(express.static('.')); 21 | 22 | app.listen(8080); 23 | -------------------------------------------------------------------------------- /test/browser/test-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example tests with testharness.js for Travis CI 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/browser/test-goal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example tests with testharness.js for Travis CI 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/browser/test-html.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const fs = require('fs'); 19 | const path = require('path'); 20 | const child = require('child_process'); 21 | const puppeteer = require('puppeteer'); 22 | const jsdom = require('jsdom'); 23 | const async = require('async'); 24 | const Mocha = require('mocha'); 25 | const htmlTests = require('./html_list.json').htmls; 26 | 27 | let bridgePath = path.join(`{__dirname}/../`, 'bin', 'rosbridge.js'); 28 | let bridge = child.fork(bridgePath); 29 | let server = child.fork(`${__dirname}/server.js`, {cwd: `${__dirname}`}); 30 | 31 | let browserTestResults = []; 32 | async.each(htmlTests, async function (html, callback) { 33 | let browser = await puppeteer.launch({args: ['--no-sandbox']}); 34 | let page = await browser.newPage(); 35 | await page.setViewport({width: 1280, height: 960}); 36 | await page.goto('http://127.0.0.1:8080/' + html.suite, {waitUntil: 'load'}); 37 | await page.waitFor(html.waitingTime); 38 | 39 | let results = await page.evaluate(() => { 40 | return document.querySelector('#results').outerHTML; 41 | }); 42 | 43 | let dom = new jsdom.JSDOM(results, { contentType: 'text/html'}); 44 | let testSuite = {}; 45 | testSuite['suite'] = html.suite; 46 | testSuite['results'] = []; 47 | for (let i = 0; i < dom.window.document.querySelector('tbody').rows.length; i++) { 48 | let testResult = {}; 49 | testResult['caseId'] = dom.window.document.querySelector('tbody').rows[i].cells[1].textContent; 50 | testResult['result'] = dom.window.document.querySelector('tbody').rows[i].cells[0].textContent; 51 | testResult['message'] = dom.window.document.querySelector('tbody').rows[i].cells[2].textContent; 52 | 53 | testResult['component'] = 'roslibjs'; 54 | testResult['purpose'] = ''; 55 | testResult['type'] = 'auto'; 56 | testResult['comment'] = ''; 57 | 58 | testSuite['results'].push(testResult); 59 | } 60 | 61 | browserTestResults.push(testSuite); 62 | await browser.close(); 63 | }, function () { 64 | server.kill('SIGINT'); 65 | bridge.kill('SIGINT'); 66 | 67 | let testBrowser = "'use strict';\n"; 68 | testBrowser += "const assert = require('assert');\n\n"; 69 | testBrowser += "describe('Roslibjs API testing over ros2-web-bridge', function() {\n"; 70 | browserTestResults.forEach((thisSuite, index) => { 71 | testBrowser += " describe('" + thisSuite.suite + "', function() {\n"; 72 | thisSuite['results'].forEach((thisResult, index) => { 73 | testBrowser += " it('" + thisResult.caseId + "', function() {\n"; 74 | testBrowser += " assert.strictEqual('" + thisResult.result 75 | + "', 'Pass', String.raw`" 76 | + thisResult.message + "`);\n"; 77 | testBrowser += " });\n"; 78 | }); 79 | testBrowser += " });\n\n"; 80 | }); 81 | testBrowser += "});\n"; 82 | 83 | fs.writeFile(path.join(`${__dirname}`, 'test-browser.js'), testBrowser, 'utf8', (err) => { 84 | if (err) throw err; 85 | 86 | let mocha = new Mocha(); 87 | mocha.addFile(path.join(`${__dirname}`, 'test-browser.js')); 88 | mocha.run(function(failures){ 89 | process.on('exit', function () { 90 | process.exit(failures); 91 | }); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/browser/test-ros.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example tests with testharness.js for Travis CI 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /test/browser/test-service.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example tests with testharness.js for Travis CI 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /test/browser/test-topic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example tests with testharness.js for Travis CI 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /test/nodejs/protocol/entry-client-mode.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const child = require('child_process'); 19 | const path = require('path'); 20 | const WebSocket = require('ws'); 21 | const TEST_PORT = 9091; 22 | 23 | var rosbridge = path.resolve(__dirname, '../../../bin/rosbridge.js'); 24 | 25 | describe('Rosbridge client mode', function() { 26 | var server; 27 | var bridgeClient; 28 | this.timeout(5 * 1000); 29 | 30 | function startBridge() { 31 | bridgeClient = child.fork(rosbridge, 32 | ['--address=ws://localhost:'+TEST_PORT], 33 | {silent: true} 34 | ); 35 | } 36 | 37 | after(function() { 38 | if (bridgeClient) { 39 | bridgeClient.kill(); 40 | } 41 | server.close(); 42 | }); 43 | 44 | it('can connect to ws server and run command', function() { 45 | return new Promise((resolve, reject) => { 46 | server = new WebSocket.Server({port: TEST_PORT}, function() { 47 | server.on('error', (err) => { 48 | console.log(err); 49 | }); 50 | server.on('connection', function(ws) { 51 | let msg = { 52 | op: 'publish', 53 | id: 'publish:/example_topic:1', 54 | topic: '/example_topic', 55 | msg: { 56 | data: 'hello from ros2bridge 0' 57 | }, 58 | latch: false 59 | }; 60 | ws.on('message', function(data) { 61 | var response = JSON.parse(data); 62 | assert.deepStrictEqual(response.level, 'error'); 63 | ws.close(); 64 | resolve(); 65 | }); 66 | // A long-standing race condition in WS handling of 67 | // new connections prevents us from sending messages 68 | // immediately after startup 69 | // More details at https://github.com/websockets/ws/issues/1393 70 | setTimeout(() => ws.send(JSON.stringify(msg)), 10); 71 | }); 72 | startBridge(); 73 | }); 74 | }); 75 | }); 76 | }); 77 | 78 | -------------------------------------------------------------------------------- /test/nodejs/protocol/entry.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const child = require('child_process'); 19 | const path = require('path'); 20 | const WebSocket = require('ws'); 21 | 22 | var rosbridge = path.resolve(__dirname, '../../../bin/rosbridge.js'); 23 | 24 | describe('Rosbridge v2.0 protocol testing', function() { 25 | var webSocketServer; 26 | this.timeout(5 * 1000); 27 | 28 | before(function(done) { 29 | webSocketServer = child.fork(rosbridge, ['-l', 'none'], {silent: true}); 30 | webSocketServer.stdout.on('data', function(data) { 31 | done(); 32 | }); 33 | }); 34 | 35 | after(function() { 36 | webSocketServer.kill(); 37 | }); 38 | 39 | describe('sanity', function() { 40 | require('./test-ros2-protocol-workflow.js')(); 41 | }); 42 | 43 | describe('advertise operation', function() { 44 | require('./test-advertise.js')(); 45 | }); 46 | 47 | describe('advertise topic with message types', function() { 48 | require('./test-advertise-msg.js')(); 49 | }); 50 | 51 | describe('unadvertise operation', function() { 52 | require('./test-unadvertise.js')(); 53 | }); 54 | 55 | describe('publish operation', function() { 56 | require('./test-publish.js')(); 57 | }); 58 | 59 | describe('publish message with types', function() { 60 | require('./test-publish-msg.js')(); 61 | }); 62 | 63 | describe('subscribe operation', function() { 64 | require('./test-subscribe.js')(); 65 | }); 66 | 67 | describe('subscribe message with types', function() { 68 | require('./test-subscribe-msg.js')(); 69 | }); 70 | 71 | describe('unsubscribe operation', function() { 72 | require('./test-unsubscribe.js')(); 73 | }); 74 | describe('call_service operation', function() { 75 | require('./test-call-service.js')(); 76 | }); 77 | 78 | describe('advertise_service operation', function() { 79 | require('./test-advertise-service.js')(); 80 | }); 81 | 82 | describe('unadvertise_service operation', function() { 83 | require('./test-unadvertise-service.js')(); 84 | }); 85 | 86 | describe('set_level operation', function() { 87 | require('./test-set-level.js')(); 88 | }); 89 | 90 | // Disable this case temporarily, sine it gets stuck on Windows CI. 91 | // describe('response operations', function() { 92 | // require('./test-response-op.js')(); 93 | // }); 94 | 95 | describe('fuzzing operations', function() { 96 | require('./test-op-neg.js')(); 97 | }); 98 | describe('service_response operation', function() { 99 | require('./test-service-response.js')(); 100 | }); 101 | }); 102 | 103 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-advertise-msg.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const WebSocket = require('ws'); 19 | 20 | module.exports = function() { 21 | let finalStatus = 'none'; 22 | let testCasesData = [ 23 | { 24 | title: 'advertise topic with message type: Bool', 25 | advertiseMsg: {op: 'advertise', id: 'advertise_bool', topic: 'advertise_bool_topic', type: 'std_msgs/Bool'} 26 | }, 27 | { 28 | title: 'advertise topic with message type: Byte', 29 | advertiseMsg: {op: 'advertise', id: 'advertise_byte', topic: 'advertise_byte_topic', type: 'std_msgs/Byte'} 30 | }, 31 | { 32 | title: 'advertise topic with message type: Char', 33 | advertiseMsg: {op: 'advertise', id: 'advertise_char', topic: 'advertise_char_topic', type: 'std_msgs/Char'} 34 | }, 35 | { 36 | title: 'advertise topic with message type: String', 37 | advertiseMsg: {op: 'advertise', id: 'advertise_string', topic: 'advertise_string_topic', type: 'std_msgs/String'} 38 | }, 39 | { 40 | title: 'advertise topic with message type: Int8', 41 | advertiseMsg: {op: 'advertise', id: 'advertise_int8', topic: 'advertise_int8_topic', type: 'std_msgs/Int8'} 42 | }, 43 | { 44 | title: 'advertise topic with message type: UInt8', 45 | advertiseMsg: {op: 'advertise', id: 'advertise_uint8', topic: 'advertise_uint8_topic', type: 'std_msgs/UInt8'} 46 | }, 47 | { 48 | title: 'advertise topic with message type: Int16', 49 | advertiseMsg: {op: 'advertise', id: 'advertise_int16', topic: 'advertise_int16_topic', type: 'std_msgs/Int16'} 50 | }, 51 | { 52 | title: 'advertise topic with message type: UInt16', 53 | advertiseMsg: {op: 'advertise', id: 'advertise_uint16', topic: 'advertise_uint16_topic', type: 'std_msgs/UInt16'} 54 | }, 55 | { 56 | title: 'advertise topic with message type: Int32', 57 | advertiseMsg: {op: 'advertise', id: 'advertise_int32', topic: 'advertise_int32_topic', type: 'std_msgs/Int32'} 58 | }, 59 | { 60 | title: 'advertise topic with message type: UInt32', 61 | advertiseMsg: {op: 'advertise', id: 'advertise_uint32', topic: 'advertise_uint32_topic', type: 'std_msgs/UInt32'} 62 | }, 63 | { 64 | title: 'advertise topic with message type: Int64', 65 | advertiseMsg: {op: 'advertise', id: 'advertise_int64', topic: 'advertise_int64_topic', type: 'std_msgs/Int64'} 66 | }, 67 | { 68 | title: 'advertise topic with message type: UInt64', 69 | advertiseMsg: {op: 'advertise', id: 'advertise_uint64', topic: 'advertise_uint64_topic', type: 'std_msgs/UInt64'} 70 | }, 71 | { 72 | title: 'advertise topic with message type: Float32', 73 | advertiseMsg: 74 | {op: 'advertise', id: 'advertise_float32', topic: 'advertise_float32_topic', type: 'std_msgs/Float32'} 75 | }, 76 | { 77 | title: 'advertise topic with message type: Float64', 78 | advertiseMsg: 79 | {op: 'advertise', id: 'advertise_float64', topic: 'advertise_float64_topic', type: 'std_msgs/Float64'} 80 | }, 81 | { 82 | title: 'advertise topic with message type: ColorRGBA', 83 | advertiseMsg: 84 | {op: 'advertise', id: 'advertise_colorrgba', topic: 'advertise_colorrgba_topic', type: 'std_msgs/ColorRGBA'} 85 | }, 86 | { 87 | title: 'advertise topic with message type: Header', 88 | advertiseMsg: {op: 'advertise', id: 'advertise_header', topic: 'advertise_header_topic', type: 'std_msgs/Header'} 89 | }, 90 | { 91 | title: 'advertise topic with message type: JointState', 92 | advertiseMsg: 93 | { op: 'advertise', id: 'advertise_jointstate', 94 | topic: 'advertise_jointstate_topic', type: 'sensor_msgs/JointState'} 95 | } 96 | ]; 97 | 98 | testCasesData.forEach((testData, index) => { 99 | it(testData.title, function() { 100 | return new Promise((resolve, reject) => { 101 | let ws = new WebSocket('ws://127.0.0.1:9090'); 102 | 103 | ws.on('open', function() { 104 | ws.send(JSON.stringify(testData.advertiseMsg)); 105 | }); 106 | ws.on('message', function(data) { 107 | let response = JSON.parse(data); 108 | assert.deepStrictEqual(response.level, finalStatus); 109 | ws.close(); 110 | resolve(); 111 | }); 112 | }); 113 | }); 114 | }); 115 | }; 116 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-advertise-service.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const rclnodejs = require('rclnodejs'); 19 | const WebSocket = require('ws'); 20 | 21 | module.exports = function() { 22 | 23 | before(function() { 24 | return rclnodejs.init().then(() => { 25 | var node = rclnodejs.createNode('service'); 26 | var wsservice = node.createService('example_interfaces/srv/AddTwoInts', 'add_two_ints', 27 | (request, response) => { 28 | let result = response.template; 29 | result.sum = request.a + request.b; 30 | response.send(result); 31 | }); 32 | rclnodejs.spin(node); 33 | }); 34 | }); 35 | 36 | after(function() { 37 | rclnodejs.shutdown(); 38 | }); 39 | 40 | let testCasesData = [ 41 | { 42 | title: 'advertise_service positive case 1', 43 | advertiseServiceMsg: { 44 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: 'add_two_ints'}, 45 | opCount: 1, 46 | finalStatus: 'none' 47 | }, 48 | { 49 | title: 'advertise_service positive case 2', 50 | advertiseServiceMsg: { 51 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: '/add_two_ints'}, 52 | opCount: 1, 53 | finalStatus: 'none' 54 | }, 55 | { 56 | title: 'advertise_service positive case 3: ROS2 interface type format', 57 | advertiseServiceMsg: { 58 | op: 'advertise_service', type: 'example_interfaces/srv/AddTwoInts', service: 'add_two_ints'}, 59 | opCount: 1, 60 | finalStatus: 'none' 61 | }, 62 | { 63 | title: 'advertise_service negative case 1: unknown type', 64 | advertiseServiceMsg: { 65 | op: 'advertise_service', type: 'example_interfaces/Foo', service: 'add_two_ints'}, 66 | opCount: 1, 67 | finalStatus: 'error' 68 | }, 69 | { 70 | title: 'advertise_service field checking: invalid type', 71 | advertiseServiceMsg: { 72 | op: 'advertise_service', type: 42, service: 'add_two_ints'}, 73 | opCount: 1, 74 | finalStatus: 'error' 75 | }, 76 | { 77 | title: 'advertise_service field checking: invalid service', 78 | advertiseServiceMsg: { 79 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: true}, 80 | opCount: 1, 81 | finalStatus: 'error' 82 | }, 83 | { 84 | title: 'advertise_service field checking: empty service', 85 | advertiseServiceMsg: { 86 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: ''}, 87 | opCount: 1, 88 | finalStatus: 'error' 89 | }, 90 | { 91 | title: 'advertise_service field checking: invalid service: with single quote', 92 | advertiseServiceMsg: { 93 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: "'add_two_ints'"}, 94 | opCount: 1, 95 | finalStatus: 'error' 96 | }, 97 | { 98 | title: 'advertise_service field checking: invalid service: with double quotes', 99 | advertiseServiceMsg: { 100 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: '"add_two_ints"'}, 101 | opCount: 1, 102 | finalStatus: 'error' 103 | }, 104 | { 105 | title: 'advertise_service field checking: invalid service: unicode', 106 | advertiseServiceMsg: { 107 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: '\u8bdd\u9898'}, 108 | opCount: 1, 109 | finalStatus: 'error' 110 | } 111 | ]; 112 | 113 | testCasesData.forEach((testData, index) => { 114 | it(testData.title, function(done) { 115 | let ws = new WebSocket('ws://127.0.0.1:9090'); 116 | 117 | ws.on('open', function() { 118 | ws.send(JSON.stringify(testData.advertiseServiceMsg)); 119 | }); 120 | ws.on('message', function(data) { 121 | let response = JSON.parse(data); 122 | assert.deepStrictEqual(response.level, testData.finalStatus); 123 | ws.close(); 124 | done(); 125 | }); 126 | }); 127 | }); 128 | }; 129 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-advertise.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const WebSocket = require('ws'); 19 | 20 | module.exports = function() { 21 | let testCasesData = [ 22 | { 23 | title: 'advertise positive case 1', 24 | advertiseMsg1: {op: 'advertise', id: 'advertise_id1', topic: 'advertise_topic1', type: 'std_msgs/String'}, 25 | opCount: 1, 26 | finalStatus: 'none' 27 | }, 28 | { 29 | title: 'advertise positive case 2', 30 | advertiseMsg1: {op: 'advertise', topic: 'advertise_topic2', type: 'std_msgs/String'}, 31 | opCount: 1, 32 | finalStatus: 'none' 33 | }, 34 | { 35 | title: 'advertise positive case 3', 36 | advertiseMsg1: {op: 'advertise', id: 'advertise_id3', topic: 'advertise_topic3', type: 'std_msgs/String'}, 37 | advertiseMsg2: {op: 'advertise', id: 'advertise_id4', topic: 'advertise_topic3', type: 'std_msgs/String'}, 38 | opCount: 2, 39 | finalStatus: 'none' 40 | }, 41 | { 42 | title: 'advertise positive case 4: ROS2 message type format', 43 | advertiseMsg1: { 44 | op: 'advertise', id: 'advertise_ros2_msg', topic: 'advertise_ros2_msg_topic', type: 'std_msgs/msg/Byte'}, 45 | opCount: 1, 46 | finalStatus: 'none' 47 | }, 48 | { 49 | title: 'advertise negative case 1', 50 | advertiseMsg1: {op: 'advertise', id: 'advertise_id5', topic: 'advertise_topic5', type: 'std_msgs/String'}, 51 | advertiseMsg2: {op: 'advertise', id: 'advertise_id6', topic: 'advertise_topic5', type: 'std_msgs/Char'}, 52 | opCount: 2, 53 | finalStatus: 'error' 54 | }, 55 | { 56 | title: 'advertise negative case 2', 57 | advertiseMsg1: {op: 'advertise', id: 'advertise_id7', topic: 'advertise_topic7', type: 'std_msgs/Foo'}, 58 | opCount: 1, 59 | finalStatus: 'error' 60 | }, 61 | { 62 | title: 'advertise field checking case 1: invalid topic', 63 | advertiseMsg1: {op: 'advertise', id: 'advertise_id8', topic: 42, type: 'std_msgs/String'}, 64 | opCount: 1, 65 | finalStatus: 'error' 66 | }, 67 | { 68 | title: 'advertise field checking case 2: invalid type', 69 | advertiseMsg1: {op: 'advertise', id: 'advertise_id9', topic: 'advertise_topic9', type: true}, 70 | opCount: 1, 71 | finalStatus: 'error' 72 | }, 73 | { 74 | title: 'advertise field checking case 3: topic cannot be empty', 75 | advertiseMsg1: {op: 'advertise', id: 'advertise_id10', topic: '', type: 'std_msgs/String'}, 76 | opCount: 1, 77 | finalStatus: 'error' 78 | }, 79 | { 80 | title: 'advertise field checking case 4: topic cannot contain single quote', 81 | advertiseMsg1: {op: 'advertise', id: 'advertise_id11', topic: "'single_advertise_topic11'", 82 | type: 'std_msgs/String'}, 83 | opCount: 1, 84 | finalStatus: 'error' 85 | }, 86 | { 87 | title: 'advertise field checking case 5: topic cannot contain double quotes', 88 | advertiseMsg1: {op: 'advertise', id: 'advertise_id12', topic: '"double_advertise_topic12"', 89 | type: 'std_msgs/String'}, 90 | opCount: 1, 91 | finalStatus: 'error' 92 | }, 93 | { 94 | title: 'advertise field checking case 6: topic does not support unicode', 95 | advertiseMsg1: {op: 'advertise', id: 'advertise_id12', topic: '\u8bdd\u9898', 96 | type: 'std_msgs/String'}, 97 | opCount: 1, 98 | finalStatus: 'error' 99 | } 100 | ]; 101 | 102 | testCasesData.forEach((testData, index) => { 103 | it(testData.title, function() { 104 | return new Promise((resolve, reject) => { 105 | let ws = new WebSocket('ws://127.0.0.1:9090'); 106 | let counter = 0; 107 | 108 | ws.on('open', function() { 109 | ws.send(JSON.stringify(testData.advertiseMsg1)); 110 | counter++; 111 | }); 112 | ws.on('message', function(data) { 113 | if (counter === testData.opCount) { 114 | let response = JSON.parse(data); 115 | assert.deepStrictEqual(response.level, testData.finalStatus); 116 | counter++; 117 | ws.close(); 118 | resolve(); 119 | } 120 | 121 | if (counter === 1) { 122 | if (testData.advertiseMsg2 !== undefined) { 123 | ws.send(JSON.stringify(testData.advertiseMsg2)); 124 | } 125 | counter++; 126 | } 127 | }); 128 | }); 129 | }); 130 | }); 131 | }; 132 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-call-service.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | /* eslint-disable camelcase */ 18 | 19 | const assert = require('assert'); 20 | const rclnodejs = require('rclnodejs'); 21 | const WebSocket = require('ws'); 22 | 23 | module.exports = function() { 24 | 25 | before(function() { 26 | return rclnodejs.init().then(() => { 27 | var node = rclnodejs.createNode('service'); 28 | var wsservice = node.createService('example_interfaces/srv/AddTwoInts', 'add_two_ints', 29 | (request, response) => { 30 | let result = response.template; 31 | result.sum = request.a + request.b; 32 | response.send(result); 33 | }); 34 | rclnodejs.spin(node); 35 | }); 36 | }); 37 | 38 | after(function() { 39 | rclnodejs.shutdown(); 40 | }); 41 | 42 | let testCasesData = [ 43 | { 44 | title: 'call_service positive case 1: full fields', 45 | callServiceMsg: { op: 'call_service', id: 'call_service_id1', service: 'add_two_ints', 46 | args: {a: 1, b: 2}, 47 | type: 'example_interfaces/AddTwoInts', 48 | fragment_size: 1, compression: 'none' 49 | }, 50 | responseCount: 2, 51 | opStatus: 'none', 52 | expectedResponse: {op: 'service_response', result: true, sum: 3} 53 | }, 54 | { 55 | title: 'call_service positive case 2: full fields with full service name', 56 | callServiceMsg: { op: 'call_service', id: 'call_service_id2', service: '/add_two_ints', 57 | args: {a: 1, b: 2}, 58 | type: 'example_interfaces/AddTwoInts', 59 | fragment_size: 1, compression: 'none' 60 | }, 61 | responseCount: 2, 62 | opStatus: 'none', 63 | expectedResponse: {op: 'service_response', result: true, sum: 3} 64 | }, 65 | { 66 | title: 'call_service positive case 3: no id field', 67 | callServiceMsg: { op: 'call_service', service: 'add_two_ints', 68 | args: {a: 3, b: 4}, 69 | type: 'example_interfaces/AddTwoInts', 70 | fragment_size: 1, compression: 'none' 71 | }, 72 | responseCount: 2, 73 | opStatus: 'none', 74 | expectedResponse: {op: 'service_response', result: true, sum: 7} 75 | }, 76 | { 77 | title: 'call_service positive case 4: no fragment_size field', 78 | callServiceMsg: { op: 'call_service', id: 'call_service_id4', service: 'add_two_ints', 79 | args: {a: 5, b: 6}, type: 'example_interfaces/AddTwoInts', compression: 'none' 80 | }, 81 | responseCount: 2, 82 | opStatus: 'none', 83 | expectedResponse: {op: 'service_response', result: true, sum: 11} 84 | }, 85 | { 86 | title: 'call_service positive case 5: no compression field', 87 | callServiceMsg: { op: 'call_service', id: 'call_service_id5', service: 'add_two_ints', 88 | args: {a: 7, b: 8}, type: 'example_interfaces/AddTwoInts', fragment_size: 1 89 | }, 90 | responseCount: 2, 91 | opStatus: 'none', 92 | expectedResponse: {op: 'service_response', result: true, sum: 15} 93 | }, 94 | { 95 | title: 'call_service negative case 1: args without type information', 96 | callServiceMsg: { op: 'call_service', id: 'call_service_id6', service: 'add_two_ints', 97 | args: {a: 9, b: 10}, fragment_size: 1, compression: 'none'}, 98 | responseCount: 1, 99 | opStatus: 'error' 100 | }, 101 | { 102 | title: 'call_service negative case 2: unknown service', 103 | callServiceMsg: { op: 'call_service', id: 'call_service_id7', service: 'add_two_float', 104 | args: {a: 11, b: 12}, type: 'example_interfaces/AddTwoInts', 105 | fragment_size: 1, compression: 'none' 106 | }, 107 | responseCount: 1, 108 | opStatus: 'none' 109 | }, 110 | { 111 | title: 'call_service field checking: invalid service', 112 | callServiceMsg: { op: 'call_service', id: 'call_service_id8', service: 42, 113 | args: {a: 13, b: 14}, type: 'example_interfaces/AddTwoInts', 114 | fragment_size: 1, compression: 'none' 115 | }, 116 | responseCount: 1, 117 | opStatus: 'error' 118 | }, 119 | { 120 | title: 'call_service field checking: invalid args', 121 | callServiceMsg: { op: 'call_service', id: 'call_service_id9', service: 'add_two_ints', 122 | args: 'invalid arguments', fragment_size: 1, compression: 'none' 123 | }, 124 | responseCount: 1, 125 | opStatus: 'error' 126 | } 127 | ]; 128 | 129 | testCasesData.forEach((testData, index) => { 130 | it(testData.title, function(done) { 131 | let ws = new WebSocket('ws://127.0.0.1:9090'); 132 | 133 | ws.on('open', function() { 134 | ws.send(JSON.stringify(testData.callServiceMsg)); 135 | }); 136 | ws.on('message', function(data) { 137 | console.log(data); 138 | let response = JSON.parse(data); 139 | 140 | if (response.op === 'status') { 141 | assert.deepStrictEqual(response.level, testData.opStatus); 142 | if (testData.responseCount === 1) { 143 | ws.close(); 144 | done(); 145 | } 146 | } 147 | if (testData.expectedResponse && response.op === testData.expectedResponse.op) { 148 | assert.deepStrictEqual(response.result, testData.expectedResponse.result); 149 | assert.deepEqual(response.values.sum, testData.expectedResponse.sum); 150 | ws.close(); 151 | done(); 152 | } 153 | }); 154 | }); 155 | }); 156 | }; 157 | 158 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-op-neg.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | 18 | const assert = require('assert'); 19 | const rclnodejs = require('rclnodejs'); 20 | const WebSocket = require('ws'); 21 | 22 | module.exports = function() { 23 | 24 | let testCasesData = [ 25 | { 26 | title: 'Negative operation case 1: unknown operation', 27 | msg: { op: 'foo'}, 28 | finalStatus: 'error' 29 | }, 30 | { 31 | title: 'Negative operation case 2: no necessary operation', 32 | msg: { op: 'advertise', bar: 'bar'}, 33 | finalStatus: 'error' 34 | }, 35 | { 36 | title: 'Negative operation case 3: no op field', 37 | msg: { baz: 'baz'}, 38 | finalStatus: 'error' 39 | } 40 | ]; 41 | 42 | testCasesData.forEach((testData, index) => { 43 | it(testData.title, function(done) { 44 | let ws = new WebSocket('ws://127.0.0.1:9090'); 45 | let counter = 0; 46 | 47 | ws.on('open', function() { 48 | ws.send(JSON.stringify(testData.msg)); 49 | }); 50 | ws.on('message', function(data) { 51 | let response = JSON.parse(data); 52 | 53 | assert.deepStrictEqual(response.level, 'error'); 54 | ws.close(); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-publish.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the 'License'); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an 'AS IS' BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const WebSocket = require('ws'); 19 | 20 | module.exports = function() { 21 | let testCasesData = [ 22 | { 23 | title: 'publish positive case 1', 24 | advertiseMsg: {op: 'advertise', id: 'publish_advertise_setup1', topic: 'publish_topic1', type: 'std_msgs/String'}, 25 | publishMsg: {op: 'publish', id: 'publish_id1', topic: 'publish_topic1', msg: {data: 'hello world!'}}, 26 | opCount: 2, 27 | finalStatus: 'none' 28 | }, 29 | { 30 | title: 'publish positive case 2', 31 | advertiseMsg: {op: 'advertise', topic: 'publish_topic2', type: 'std_msgs/String'}, 32 | publishMsg: {op: 'publish', topic: 'publish_topic2', msg: {data: 'hello world!'}}, 33 | opCount: 2, 34 | finalStatus: 'none' 35 | }, 36 | { 37 | title: 'publish positive case 3: ROS2 message type format', 38 | advertiseMsg: {op: 'advertise', topic: 'publish_ros2_msg_topic', type: 'std_msgs/msg/String'}, 39 | publishMsg: {op: 'publish', topic: 'publish_ros2_msg_topic', msg: {data: 'hello world!'}}, 40 | opCount: 2, 41 | finalStatus: 'none' 42 | }, 43 | { 44 | title: 'publish negative case 1: topic not exist', 45 | publishMsg: {op: 'publish', id: 'publish_id3', topic: 'publish_topic3', msg: {data: 'Hello World!'}}, 46 | opCount: 1, 47 | finalStatus: 'error' 48 | }, 49 | { 50 | title: 'publish negative case 2: inconsistent message type', 51 | advertiseMsg: 52 | {op: 'advertise', id: 'publish_advertise_setup4', topic: 'publish_topic4', type: 'std_msgs/String'}, 53 | publishMsg: {op: 'publish', id: 'publish_id4', topic: 'publish_topic4', msg: {data: 42}}, 54 | opCount: 2, 55 | finalStatus: 'error' 56 | }, 57 | { 58 | title: 'publish negative case 3: msg is subset of type', 59 | advertiseMsg: {op: 'advertise', id: 'publish_advertise_setup5', topic: 'publish_topic5', type: 'std_msgs/Header'}, 60 | publishMsg: {op: 'publish', id: 'publish_id5', topic: 'publish_topic5', msg: { 61 | stamp: {sec: 123456, nanosec: 789} 62 | }}, 63 | opCount: 2, 64 | // incompatible with the spec 65 | finalStatus: 'error' 66 | }, 67 | { 68 | title: 'publish field checking case 1: invalid id', 69 | advertiseMsg: 70 | {op: 'advertise', id: 'publish_advertise_setup6', topic: 'publish_topic6', type: 'std_msgs/String'}, 71 | publishMsg: {op: 'publish', id: 42, topic: 'publish_topic6', msg: {data: 'Hello World!'}}, 72 | opCount: 2, 73 | finalStatus: 'none' 74 | }, 75 | { 76 | title: 'publish field checking case 1: invalid topic', 77 | advertiseMsg: {op: 'advertise', id: 'publish_advertise_setup7', topic: 'publish_topic7', type: 'std_msgs/String'}, 78 | publishMsg: {op: 'publish', id: 'publish_id7', topic: 42, msg: {data: 'Hello World!'}}, 79 | opCount: 2, 80 | finalStatus: 'error' 81 | }, 82 | { 83 | title: 'publish field checking case 2: msg is not a JSON object', 84 | advertiseMsg: 85 | { op: 'advertise', id: 'publish_advertise_setup8', topic: 'publish_topic8', type: 'std_msgs/String'}, 86 | publishMsg: {op: 'publish', id: 'publish_id8', topic: 'publish_topic8', msg: 42}, 87 | opCount: 2, 88 | finalStatus: 'error' 89 | } 90 | ]; 91 | 92 | testCasesData.forEach((testData, index) => { 93 | it(testData.title, function() { 94 | return new Promise((resolve, reject) => { 95 | let ws = new WebSocket('ws://127.0.0.1:9090'); 96 | var counter = 0; 97 | 98 | ws.on('open', function() { 99 | if (testData.advertiseMsg !== undefined) { 100 | ws.send(JSON.stringify(testData.advertiseMsg)); 101 | } 102 | counter++; 103 | if (testData.opCount === 1) { 104 | ws.send(JSON.stringify(testData.publishMsg)); 105 | counter++; 106 | } 107 | }); 108 | ws.on('message', function(data) { 109 | if (counter === 2) { 110 | let response = JSON.parse(data); 111 | assert.deepStrictEqual(response.level, testData.finalStatus); 112 | ws.close(); 113 | resolve(); 114 | } 115 | if (counter === 1) { 116 | ws.send(JSON.stringify(testData.publishMsg)); 117 | counter++; 118 | } 119 | }); 120 | }); 121 | }); 122 | }); 123 | }; 124 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-response-op.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const rclnodejs = require('rclnodejs'); 19 | const WebSocket = require('ws'); 20 | 21 | module.exports = function() { 22 | before(function() { 23 | return new Promise((resolve, reject) => { 24 | rclnodejs.init().then(() => { 25 | var node = rclnodejs.createNode('service'); 26 | var wsservice = node.createService('example_interfaces/srv/AddTwoInts', 'add_two_ints_resp', 27 | (request, response) => { 28 | let result = response.template; 29 | result.sum = request.a + request.b; 30 | response.send(result); 31 | }); 32 | rclnodejs.spin(node); 33 | setTimeout(() => { 34 | resolve(); 35 | }, 2000); 36 | }); 37 | }); 38 | }); 39 | 40 | after(function() { 41 | rclnodejs.shutdown(); 42 | }); 43 | 44 | let testCasesData = [ 45 | { 46 | title: 'Common responses should contain set_level field', 47 | msg: { 48 | op: 'advertise', id: 'advertise_topic_resp_id1', topic: 'advertise_topic_resp', type: 'std_msgs/String'}, 49 | opType: 'common', 50 | respCount: 1, 51 | finalStatus: 'none' 52 | }, 53 | { 54 | title: 'service_response status field checking', 55 | msg: {op: 'call_service', service: 'add_two_ints_resp', id: 'call_service_id1', 56 | args: {a: 1, b: 2}, type: 'example_interfaces/AddTwoInts'}, 57 | opType: 'service', 58 | respCount: 2, 59 | expectedResponse: {op: 'service_response', result: true, sum: 3} 60 | } 61 | ]; 62 | 63 | testCasesData.forEach((testData, index) => { 64 | it(testData.title, function(done) { 65 | let ws = new WebSocket('ws://127.0.0.1:9090'); 66 | let count = 0; 67 | ws.on('open', function() { 68 | ws.send(JSON.stringify(testData.msg)); 69 | count++; 70 | }); 71 | ws.on('message', function(data) { 72 | let response = JSON.parse(data); 73 | if (testData.opType === 'common' && testData.respCount === 1) { 74 | assert.deepStrictEqual(response.op, 'set_level'); 75 | assert.deepStrictEqual(typeof response.level, 'string'); 76 | ws.close(); 77 | done(); 78 | } 79 | 80 | if (testData.opType === 'service' && response.op === testData.expectedResponse.op) { 81 | assert.deepStrictEqual(response.op, 'service_response'); 82 | assert.deepStrictEqual(typeof response.service, 'string'); 83 | assert.deepStrictEqual(typeof response.result, 'boolean'); 84 | ws.close(); 85 | done(); 86 | } 87 | }); 88 | }); 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-ros2-protocol-workflow.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const WebSocket = require('ws'); 19 | 20 | module.exports = function() { 21 | it('Protocol testing general workflow', function() { 22 | return new Promise((resolve, reject) => { 23 | let ws = new WebSocket('ws://127.0.0.1:9090'); 24 | ws.on('open', function() { 25 | let msg = { 26 | op: 'publish', 27 | id: 'publish:/example_topic:1', 28 | topic: '/example_topic', 29 | msg: { 30 | data: 'hello from ros2bridge 0' 31 | }, 32 | latch: false 33 | }; 34 | ws.send(JSON.stringify(msg)); 35 | }); 36 | ws.on('message', function(data) { 37 | var response = JSON.parse(data); 38 | assert.deepStrictEqual(response.level, 'error'); 39 | ws.close(); 40 | resolve(); 41 | }); 42 | }); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-service-response.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the 'License'); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an 'AS IS' BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const rclnodejs = require('rclnodejs'); 19 | const WebSocket = require('ws'); 20 | 21 | module.exports = function() { 22 | before(function() { 23 | return rclnodejs.init().then(() => { 24 | var node = rclnodejs.createNode('service'); 25 | var wsservice = node.createService('example_interfaces/srv/AddTwoInts', 'add_two_ints', 26 | (request, response) => { 27 | let result = response.template; 28 | result.sum = request.a + request.b; 29 | response.send(result); 30 | }); 31 | rclnodejs.spin(node); 32 | }); 33 | }); 34 | 35 | after(function() { 36 | rclnodejs.shutdown(); 37 | }); 38 | 39 | let testCasesData = [ 40 | { 41 | title: 'service_response positive case 1', 42 | serviceResponseMsg: { 43 | op: 'service_response', id: 'service_response_id1', service: '/add_two_ints', 44 | values: {sum: 1}, result: true}, 45 | opCount: 1, 46 | finalStatus: 'none' 47 | }, 48 | { 49 | title: 'service_response positive case 2', 50 | serviceResponseMsg: { 51 | op: 'service_response', id: 'service_response_id2', service: 'add_two_ints', 52 | values: {sum: 2}, result: true}, 53 | opCount: 1, 54 | finalStatus: 'none' 55 | }, 56 | { 57 | title: 'service_response positive case 3', 58 | serviceResponseMsg: { 59 | op: 'service_response', service: 'add_two_ints', values: {sum: 3}, result: true}, 60 | opCount: 1, 61 | finalStatus: 'none' 62 | }, 63 | { 64 | title: 'service_response positive case 4', 65 | serviceResponseMsg: { 66 | op: 'service_response', id: 'service_response_id4', service: 'add_two_ints', result: true}, 67 | opCount: 1, 68 | finalStatus: 'none' 69 | }, 70 | { 71 | title: 'service_response positive case 5', 72 | serviceResponseMsg: { 73 | op: 'service_response', service: 'add_two_ints', result: true}, 74 | opCount: 1, 75 | finalStatus: 'none' 76 | }, 77 | { 78 | title: 'service_response positive case 6', 79 | serviceResponseMsg: { 80 | op: 'service_response', service: '/add_two_ints', result: true}, 81 | opCount: 1, 82 | finalStatus: 'none' 83 | }, 84 | { 85 | title: 'service_response negative case 1', 86 | serviceResponseMsg: { 87 | op: 'service_response', values: {sum: 6}, result: false}, 88 | opCount: 1, 89 | finalStatus: 'none' 90 | }, 91 | { 92 | title: 'service_response negative case 2', 93 | serviceResponseMsg: { 94 | op: 'service_response', service: 'add_two_ints', values: {sum: 7}}, 95 | opCount: 1, 96 | finalStatus: 'none' 97 | } 98 | ]; 99 | 100 | testCasesData.forEach((testData, index) => { 101 | it(testData.title, function(done) { 102 | let ws = new WebSocket('ws://127.0.0.1:9090'); 103 | let count = 0; 104 | 105 | ws.on('open', function() { 106 | ws.send(JSON.stringify(testData.serviceResponseMsg)); 107 | count++; 108 | }); 109 | ws.on('message', function(data) { 110 | // console.log(`${count}`, data); 111 | let response = JSON.parse(data); 112 | 113 | if (testData.opCount === 1) { 114 | assert.deepStrictEqual(response.level, testData.finalStatus); 115 | ws.close(); 116 | done(); 117 | } 118 | count++; 119 | }); 120 | }); 121 | }); 122 | }; 123 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-set-level.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const WebSocket = require('ws'); 19 | 20 | module.exports = function() { 21 | let testCasesData = [ 22 | { 23 | title: 'set_level to error', 24 | ops: [ 25 | { 26 | payload: {op: 'set_level', id: 'id1', level: 'error'}, 27 | status: null 28 | } 29 | ], 30 | }, 31 | { 32 | title: 'set_level to warning', 33 | ops: [ 34 | { 35 | payload: {op: 'set_level', id: 'id1', level: 'warning'}, 36 | status: null 37 | } 38 | ], 39 | }, 40 | { 41 | title: 'set_level to info', 42 | ops: [ 43 | { 44 | payload: {op: 'set_level', id: 'id1', level: 'info'}, 45 | status: null 46 | } 47 | ], 48 | }, 49 | { 50 | title: 'set_level to none', 51 | ops: [ 52 | { 53 | payload: {op: 'set_level', id: 'id1', level: 'none'}, 54 | status: 'none' 55 | } 56 | ], 57 | }, 58 | { 59 | title: 'set_level to invalid', 60 | ops: [ 61 | { 62 | payload: {op: 'set_level', id: 'id1', level: 'invalid'}, 63 | status: 'error' 64 | } 65 | ], 66 | }, 67 | ]; 68 | 69 | testCasesData.forEach((testData, index) => { 70 | it(testData.title, function() { 71 | return new Promise((resolve, reject) => { 72 | let ws = new WebSocket('ws://127.0.0.1:9090'); 73 | let counter = 0; 74 | let timeout = null; 75 | 76 | function handleMessage(data) { 77 | if (timeout !== null) { 78 | clearTimeout(timeout); 79 | timeout = null; 80 | } 81 | if (data !== null || testData.ops[counter].status !== null) { 82 | let response = JSON.parse(data); 83 | assert.deepStrictEqual(response.level, testData.ops[counter].status); 84 | } 85 | 86 | counter++; 87 | if (counter === testData.ops.length) { 88 | ws.close(); 89 | resolve(); 90 | } else { 91 | ws.send(JSON.stringify(testData.ops[counter].payload)); 92 | } 93 | } 94 | ws.on('message', handleMessage); 95 | 96 | ws.on('open', function() { 97 | ws.send(JSON.stringify(testData.ops[0].payload)); 98 | if (testData.ops[0].status === null) { 99 | timeout = setTimeout(() => handleMessage(null), 100); 100 | } 101 | }); 102 | }); 103 | }); 104 | }); 105 | }; 106 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-subscribe-msg.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | /* eslint-disable camelcase */ 18 | 19 | const assert = require('assert'); 20 | const WebSocket = require('ws'); 21 | 22 | module.exports = function() { 23 | 24 | let testCasesData = [ 25 | { 26 | title: 'subscribe message with type: Bool', 27 | msg0: {op: 'subscribe', id: 'subscribe_bool', topic: 'subscribe_bool_topic', type: 'std_msgs/Bool'}, 28 | msg1: {op: 'advertise', id: 'advertise_setup_bool', topic: 'subscribe_bool_topic', type: 'std_msgs/Bool'}, 29 | msg2: {op: 'publish', id: 'publish_setup_bool', topic: 'subscribe_bool_topic', msg: {data: true}}, 30 | expectedData: true 31 | }, 32 | { 33 | title: 'subscribe message with type: Byte', 34 | msg0: {op: 'subscribe', id: 'subscribe_byte', topic: 'subscribe_byte_topic', type: 'std_msgs/Byte'}, 35 | msg1: {op: 'advertise', id: 'advertise_setup_byte', topic: 'subscribe_byte_topic', type: 'std_msgs/Byte'}, 36 | msg2: {op: 'publish', id: 'publish_setup_byte', topic: 'subscribe_byte_topic', msg: {data: 0xff}}, 37 | expectedData: 255 38 | }, 39 | { 40 | title: 'subscribe message with type: Char', 41 | msg0: {op: 'subscribe', id: 'subscribe_char', topic: 'subscribe_char_topic', type: 'std_msgs/Char'}, 42 | msg1: {op: 'advertise', id: 'advertise_setup_char', topic: 'subscribe_char_topic', type: 'std_msgs/Char'}, 43 | msg2: {op: 'publish', id: 'publish_setup_char', topic: 'subscribe_char_topic', msg: {data: 'A'}}, 44 | expectedData: 65 45 | }, 46 | { 47 | title: 'subscribe message with type: String', 48 | msg0: {op: 'subscribe', id: 'subscribe_string', topic: 'subscribe_string_topic', type: 'std_msgs/String'}, 49 | msg1: {op: 'advertise', id: 'advertise_setup_string', topic: 'subscribe_string_topic', type: 'std_msgs/String'}, 50 | msg2: {op: 'publish', id: 'publish_setup_string', topic: 'subscribe_string_topic', msg: {data: 'hello world!'}}, 51 | expectedData: 'hello world!' 52 | }, 53 | { 54 | title: 'subscribe message with type: Int8', 55 | msg0: {op: 'subscribe', id: 'advertise_int8', topic: 'subscribe_int8_topic', type: 'std_msgs/Int8'}, 56 | msg1: {op: 'advertise', id: 'advertise_setup_int8', topic: 'subscribe_int8_topic', type: 'std_msgs/Int8'}, 57 | msg2: {op: 'publish', id: 'publish_setup_int8', topic: 'subscribe_int8_topic', msg: {data: -0x80}}, 58 | expectedData: -128 59 | }, 60 | { 61 | title: 'subscribe message with type: UInt8', 62 | msg0: {op: 'subscribe', id: 'subscribe_uint8', topic: 'subscribe_uint8_topic', type: 'std_msgs/UInt8'}, 63 | msg1: {op: 'advertise', id: 'advertise_setup_uint8', topic: 'subscribe_uint8_topic', type: 'std_msgs/UInt8'}, 64 | msg2: {op: 'publish', id: 'publish_setup_uint8', topic: 'subscribe_uint8_topic', msg: {data: 0xff}}, 65 | expectedData: 255 66 | }, 67 | { 68 | title: 'subscribe message with type: Int16', 69 | msg0: {op: 'subscribe', id: 'subscribe_int16', topic: 'subscribe_int16_topic', type: 'std_msgs/Int16'}, 70 | msg1: {op: 'advertise', id: 'advertise_setup_int16', topic: 'subscribe_int16_topic', type: 'std_msgs/Int16'}, 71 | msg2: {op: 'publish', id: 'publish_setup_int16', topic: 'subscribe_int16_topic', msg: {data: -0x8000}}, 72 | expectedData: -0x8000 73 | }, 74 | { 75 | title: 'subscribe message with type: UInt16', 76 | msg0: {op: 'subscribe', id: 'subscribe_uint16', topic: 'subscribe_uint16_topic', type: 'std_msgs/UInt16'}, 77 | msg1: {op: 'advertise', id: 'advertise_setup_uint16', topic: 'subscribe_uint16_topic', type: 'std_msgs/UInt16'}, 78 | msg2: {op: 'publish', id: 'publish_setup_uint16', topic: 'subscribe_uint16_topic', msg: {data: 0xffff}}, 79 | expectedData: 0xffff 80 | }, 81 | { 82 | title: 'subscribe message with type: Int32', 83 | msg0: {op: 'subscribe', id: 'subscribe_int32', topic: 'subscribe_int32_topic', type: 'std_msgs/Int32'}, 84 | msg1: {op: 'advertise', id: 'advertise_setup_int32', topic: 'subscribe_int32_topic', type: 'std_msgs/Int32'}, 85 | msg2: {op: 'publish', id: 'publish_setup_int32', topic: 'subscribe_int32_topic', msg: {data: -0x80000000}}, 86 | expectedData: -0x80000000 87 | }, 88 | { 89 | title: 'subscribe message with type: UInt32', 90 | msg0: {op: 'subscribe', id: 'subscribe_uint32', topic: 'subscribe_uint32_topic', type: 'std_msgs/UInt32'}, 91 | msg1: {op: 'advertise', id: 'advertise_setup_uint32', topic: 'subscribe_uint32_topic', type: 'std_msgs/UInt32'}, 92 | msg2: {op: 'publish', id: 'publish_setup_uint32', topic: 'subscribe_uint32_topic', msg: {data: 0xffffffff}}, 93 | expectedData: 0xffffffff 94 | }, 95 | { 96 | title: 'subscribe message with type: Int64', 97 | msg0: {op: 'subscribe', id: 'subscribe_int64', topic: 'subscribe_int64_topic', type: 'std_msgs/Int64'}, 98 | msg1: {op: 'advertise', id: 'advertise_setup_int64', topic: 'subscribe_int64_topic', type: 'std_msgs/Int64'}, 99 | msg2: { 100 | op: 'publish', id: 'publish_setup_int64', topic: 'subscribe_int64_topic', 101 | msg: {data: Number.MIN_SAFE_INTEGER}}, 102 | expectedData: Number.MIN_SAFE_INTEGER 103 | }, 104 | { 105 | title: 'subscribe message with type: UInt64', 106 | msg0: {op: 'subscribe', id: 'subscribe_uint64', topic: 'subscribe_uint64_topic', type: 'std_msgs/UInt64'}, 107 | msg1: {op: 'advertise', id: 'advertise_setup_uint64', topic: 'subscribe_uint64_topic', type: 'std_msgs/UInt64'}, 108 | msg2: { 109 | op: 'publish', id: 'publish_setup_uint64', topic: 'subscribe_uint64_topic', 110 | msg: {data: Number.MAX_SAFE_INTEGER} 111 | }, 112 | expectedData: Number.MAX_SAFE_INTEGER 113 | }, 114 | { 115 | title: 'subscribe message with type: Float32', 116 | msg0: {op: 'subscribe', id: 'subscribe_float32', topic: 'subscribe_float32_topic', type: 'std_msgs/Float32'}, 117 | msg1: { 118 | op: 'advertise', id: 'advertise_setup_float32', topic: 'subscribe_float32_topic', type: 'std_msgs/Float32'}, 119 | msg2: {op: 'publish', id: 'publish_setup_float32', topic: 'subscribe_float32_topic', msg: {data: 3.14}}, 120 | msgType: 'float', 121 | expectedData: 3.14, 122 | precision: 0.01 123 | }, 124 | { 125 | title: 'subscribe message with type: Float64', 126 | msg0: {op: 'subscribe', id: 'subscribe_float64', topic: 'subscribe_float64_topic', type: 'std_msgs/Float64'}, 127 | msg1: { 128 | op: 'advertise', id: 'advertise_setup_float64', topic: 'subscribe_float64_topic', type: 'std_msgs/Float64'}, 129 | msg2: {op: 'publish', id: 'publish_setup_float64', topic: 'subscribe_float64_topic', msg: {data: 3.1415926}}, 130 | msgType: 'float', 131 | expectedData: 3.1415926, 132 | precision: 0.0000001 133 | }, 134 | { 135 | title: 'subscribe message with type: ColorRGBA', 136 | msg0: { 137 | op: 'subscribe', id: 'subscribe_colorrgba', topic: 'subscribe_colorrgba_topic', type: 'std_msgs/ColorRGBA'}, 138 | msg1: { 139 | op: 'advertise', id: 'advertise_setup_colorrgba', topic: 'subscribe_colorrgba_topic', 140 | type: 'std_msgs/ColorRGBA'}, 141 | msg2: { 142 | op: 'publish', id: 'publish_setup_colorrgba', topic: 'subscribe_colorrgba_topic', 143 | msg: {a: 0.5, r: 255, g: 255, b: 255}}, 144 | msgType: 'compound', 145 | expectedData: {a: 0.5, r: 255, g: 255, b: 255} 146 | }, 147 | { 148 | title: 'subscribe message with type: Header', 149 | msg0: {op: 'subscribe', id: 'subscribe_header', topic: 'subscribe_header_topic', type: 'std_msgs/Header'}, 150 | msg1: {op: 'advertise', id: 'advertise_setup_header', topic: 'subscribe_header_topic', type: 'std_msgs/Header'}, 151 | msg2: {op: 'publish', id: 'publish_setup_header', topic: 'subscribe_header_topic', 152 | msg: {stamp: {sec: 123456, nanosec: 789}, frame_id: 'main frame'}}, 153 | msgType: 'compound', 154 | expectedData: {stamp: {sec: 123456, nanosec: 789}, frame_id: 'main frame'} 155 | }, 156 | { 157 | title: 'subscribe message with type: JointState', 158 | msg0: { 159 | op: 'subscribe', id: 'subscribe_jointstate', topic: 'subscribe_jointstate_topic', 160 | type: 'sensor_msgs/JointState'}, 161 | msg1: {op: 'advertise', id: 'advertise_setup_jointstate', topic: 'subscribe_jointstate_topic', 162 | type: 'sensor_msgs/JointState'}, 163 | msg2: {op: 'publish', id: 'publish_setup_jointstate', topic: 'subscribe_jointstate_topic', 164 | msg: {header: {stamp: {sec: 123456, nanosec: 789}, frame_id: 'main frame'}, 165 | name: ['Tom', 'Jerry'], position: [1, 2], velocity: [2, 3], effort: [4, 5, 6]}}, 166 | msgType: 'compound', 167 | expectedData: {header: {stamp: {sec: 123456, nanosec: 789}, frame_id: 'main frame'}, 168 | name: ['Tom', 'Jerry'], position: [1, 2], velocity: [2, 3], effort: [4, 5, 6]} 169 | } 170 | ]; 171 | let testResults = {respCount: 4, finalStatus: 'none'}; 172 | 173 | testCasesData.forEach((testData, index) => { 174 | it(testData.title, function() { 175 | return new Promise((resolve, reject) => { 176 | let ws = new WebSocket('ws://127.0.0.1:9090'); 177 | let counter = 0; 178 | 179 | ws.on('open', function() { 180 | ws.send(JSON.stringify(testData.msg0)); 181 | counter++; 182 | }); 183 | 184 | ws.on('message', function(data) { 185 | let response = JSON.parse(data); 186 | if (counter < testResults.respCount) { 187 | assert.deepStrictEqual(response.level, testResults.finalStatus); 188 | } 189 | if (counter === testResults.respCount) { 190 | if (testData.msgType === 'float') { 191 | assert.ok(Math.abs(response.msg.data - testData.expectedData) < testData.precision); 192 | } else if (testData.msgType === 'compound') { 193 | assert.deepEqual(response.msg, testData.expectedData); 194 | } else { 195 | assert.deepStrictEqual(response.msg.data, testData.expectedData); 196 | } 197 | ws.close(); 198 | resolve(); 199 | } 200 | if (counter === 3) { 201 | counter++; 202 | } 203 | if (counter === 2) { 204 | ws.send(JSON.stringify(testData.msg2)); 205 | counter++; 206 | } 207 | if (counter === 1) { 208 | ws.send(JSON.stringify(testData.msg1)); 209 | counter++; 210 | } 211 | }); 212 | }); 213 | }); 214 | }); 215 | }; 216 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-subscribe.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | /* eslint-disable camelcase */ 18 | 19 | const assert = require('assert'); 20 | const WebSocket = require('ws'); 21 | 22 | module.exports = function() { 23 | 24 | let testCasesData = [ 25 | { 26 | title: 'subscribe positive case 1: full fields', 27 | msg0: {op: 'subscribe', id: 'subscribe_id1', topic: 'subscribe_topic1', type: 'std_msgs/String', 28 | throttle_rate: 0, queue_length: 0, fragment_size: 1, compression: 'none'}, 29 | msg1: {op: 'advertise', id: 'advertise_setup_id1', topic: 'subscribe_topic1', type: 'std_msgs/String'}, 30 | msg2: {op: 'publish', id: 'publish_setup_id1', topic: 'subscribe_topic1', msg: {data: 'subscribe operation'}}, 31 | }, 32 | { 33 | title: 'subscribe positive case 2: no field id', 34 | msg0: {op: 'subscribe', topic: 'subscribe_topic2', type: 'std_msgs/String', 35 | throttle_rate: 0, queue_length: 0, fragment_size: 1, compression: 'none'}, 36 | msg1: {op: 'advertise', id: 'advertise_setup_id2', topic: 'subscribe_topic2', type: 'std_msgs/String'}, 37 | msg2: {op: 'publish', id: 'publish_setup_id2', topic: 'subscribe_topic2', msg: {data: 'subscribe operation'}}, 38 | }, 39 | { 40 | title: 'subscribe positive case 3: no field throttle_rate', 41 | msg0: {op: 'subscribe', id: 'subscribe_id3', topic: 'subscribe_topic3', type: 'std_msgs/String', 42 | queue_length: 0, fragment_size: 1, compression: 'none'}, 43 | msg1: {op: 'advertise', id: 'advertise_setup_id3', topic: 'subscribe_topic3', type: 'std_msgs/String'}, 44 | msg2: {op: 'publish', id: 'publish_setup_id3', topic: 'subscribe_topic3', msg: {data: 'subscribe operation'}}, 45 | }, 46 | { 47 | title: 'subscribe positive case 4: no field queue_length', 48 | msg0: {op: 'subscribe', id: 'subscribe_id4', topic: 'subscribe_topic4', type: 'std_msgs/String', 49 | throttle_rate: 0, fragment_size: 1, compression: 'none'}, 50 | msg1: {op: 'advertise', id: 'advertise_setup_id4', topic: 'subscribe_topic4', type: 'std_msgs/String'}, 51 | msg2: {op: 'publish', id: 'publish_setup_id4', topic: 'subscribe_topic4', msg: {data: 'subscribe operation'}}, 52 | }, 53 | { 54 | title: 'subscribe positive case 5: no field fragment_size', 55 | msg0: {op: 'subscribe', id: 'advertise_id5', topic: 'subscribe_topic5', type: 'std_msgs/String', 56 | throttle_rate: 0, queue_length: 0, compression: 'none'}, 57 | msg1: {op: 'advertise', id: 'advertise_setup_id5', topic: 'subscribe_topic5', type: 'std_msgs/String'}, 58 | msg2: {op: 'publish', id: 'publish_setup_id5', topic: 'subscribe_topic5', msg: {data: 'subscribe operation'}}, 59 | }, 60 | { 61 | title: 'subscribe positive case 6: no field compression', 62 | msg0: {op: 'subscribe', id: 'subscribe_id6', topic: 'subscribe_topic6', type: 'std_msgs/String', 63 | throttle_rate: 0, queue_length: 0, fragment_size: 1}, 64 | msg1: {op: 'advertise', id: 'advertise_setup_id6', topic: 'subscribe_topic6', type: 'std_msgs/String'}, 65 | msg2: {op: 'publish', id: 'publish_setup_id6', topic: 'subscribe_topic6', msg: {data: 'subscribe operation'}}, 66 | } 67 | ]; 68 | let testResults = {respCount: 4, finalStatus: 'none', data: 'subscribe operation'}; 69 | 70 | testCasesData.forEach((testData, index) => { 71 | it(testData.title, function() { 72 | return new Promise((resolve, reject) => { 73 | let ws = new WebSocket('ws://127.0.0.1:9090'); 74 | let counter = 0; 75 | 76 | ws.on('open', function() { 77 | ws.send(JSON.stringify(testData.msg0)); 78 | counter++; 79 | }); 80 | 81 | ws.on('message', function(data) { 82 | let response = JSON.parse(data); 83 | if (counter === testResults.respCount) { 84 | assert.deepStrictEqual(response.msg.data, testResults.data); 85 | ws.close(); 86 | resolve(); 87 | } 88 | if (counter === 3) { 89 | counter++; 90 | } 91 | if (counter === 2) { 92 | ws.send(JSON.stringify(testData.msg2)); 93 | counter++; 94 | } 95 | if (counter === 1) { 96 | assert.deepStrictEqual(response.level, testResults.finalStatus); 97 | ws.send(JSON.stringify(testData.msg1)); 98 | counter++; 99 | } 100 | }); 101 | }); 102 | }); 103 | }); 104 | 105 | let testCasesNegData = [ 106 | { 107 | title: 'subscribe negative case 1: topic not exist', 108 | subscribeMsg: {op: 'subscribe', id: 'subscribe_neg_id1', topic: 'subscribe_neg_topic1', type: 'std_msgs/Foo'}, 109 | finalStatus: 'error' 110 | }, 111 | { 112 | // Incompatible with rosbridge v2 protocol 113 | title: 'subscribe negative case 2: type cannot be inferred', 114 | subscribeMsg: {op: 'subscribe', id: 'subscribe_neg_id2', topic: 'subscribe_neg_topic2'}, 115 | finalStatus: 'error' 116 | }, 117 | { 118 | title: 'subscribe field checking case 1: invalid topic', 119 | subscribeMsg: {op: 'subscribe', id: 'subscribe_neg_id3', topic: 42, type: 'std_msgs/String'}, 120 | finalStatus: 'error' 121 | }, 122 | { 123 | title: 'subscribe field checking case 1: invalid type', 124 | subscribeMsg: {op: 'subscribe', id: 'subscribe_neg_id3', topic: 'subscribe_neg_topic4', type: true}, 125 | finalStatus: 'error' 126 | } 127 | ]; 128 | 129 | testCasesNegData.forEach((testNegData, index) => { 130 | it(testNegData.title, function() { 131 | return new Promise((resolve, reject) => { 132 | let ws = new WebSocket('ws://127.0.0.1:9090'); 133 | 134 | ws.on('open', function() { 135 | ws.send(JSON.stringify(testNegData.subscribeMsg)); 136 | }); 137 | 138 | ws.on('message', function(data) { 139 | let response = JSON.parse(data); 140 | assert.deepStrictEqual(response.level, testNegData.finalStatus); 141 | ws.close(); 142 | resolve(); 143 | }); 144 | }); 145 | }); 146 | }); 147 | }; 148 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-unadvertise-service.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const rclnodejs = require('rclnodejs'); 19 | const WebSocket = require('ws'); 20 | 21 | module.exports = function() { 22 | 23 | before(function() { 24 | return rclnodejs.init().then(() => { 25 | var node = rclnodejs.createNode('service'); 26 | var wsservice = node.createService('example_interfaces/srv/AddTwoInts', 'add_two_ints', 27 | (request, response) => { 28 | let result = response.template; 29 | result.sum = request.a + request.b; 30 | response.send(result); 31 | }); 32 | rclnodejs.spin(node); 33 | }); 34 | }); 35 | 36 | after(function() { 37 | rclnodejs.shutdown(); 38 | }); 39 | 40 | let testCasesData = [ 41 | { 42 | title: 'unadvertise_service positive case 1', 43 | advertiseServiceMsg: { 44 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: 'add_two_ints'}, 45 | unadvertiseServiceMsg: {op: 'unadvertise_service', service: 'add_two_ints'}, 46 | opCount: 2, 47 | finalStatus: 'none' 48 | }, 49 | { 50 | title: 'unadvertise_service positive case 2', 51 | advertiseServiceMsg: { 52 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: '/add_two_ints'}, 53 | unadvertiseServiceMsg: {op: 'unadvertise_service', service: '/add_two_ints'}, 54 | opCount: 2, 55 | finalStatus: 'none' 56 | }, 57 | { 58 | title: 'unadvertise_service negative case 1: invalid service type', 59 | advertiseServiceMsg: { 60 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: 'add_two_ints'}, 61 | unadvertiseServiceMsg: {op: 'unadvertise_service', service: 42}, 62 | opCount: 2, 63 | finalStatus: 'error' 64 | }, 65 | { 66 | title: 'unadvertise_service field checking case 1: empty service', 67 | advertiseServiceMsg: { 68 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: 'add_two_ints'}, 69 | unadvertiseServiceMsg: {op: 'unadvertise_service', service: ''}, 70 | opCount: 2, 71 | finalStatus: 'error' 72 | }, 73 | { 74 | title: 'unadvertise_service field checking case 1: with single quote', 75 | advertiseServiceMsg: { 76 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: 'add_two_ints'}, 77 | unadvertiseServiceMsg: {op: 'unadvertise_service', service: "'add_two_ints'"}, 78 | opCount: 2, 79 | finalStatus: 'error' 80 | }, 81 | { 82 | title: 'unadvertise_service field checking case 1: with double quote', 83 | advertiseServiceMsg: { 84 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: 'add_two_ints'}, 85 | unadvertiseServiceMsg: {op: 'unadvertise_service', service: '"add_two_ints"'}, 86 | opCount: 2, 87 | finalStatus: 'error' 88 | }, 89 | { 90 | title: 'unadvertise_service field checking case 1: unicode', 91 | advertiseServiceMsg: { 92 | op: 'advertise_service', type: 'example_interfaces/AddTwoInts', service: 'add_two_ints'}, 93 | unadvertiseServiceMsg: {op: 'unadvertise_service', service: '\u8bdd\u9898'}, 94 | opCount: 2, 95 | finalStatus: 'error' 96 | } 97 | ]; 98 | 99 | testCasesData.forEach((testData, index) => { 100 | it(testData.title, function(done) { 101 | let ws = new WebSocket('ws://127.0.0.1:9090'); 102 | let count = 0; 103 | 104 | ws.on('open', function() { 105 | ws.send(JSON.stringify(testData.advertiseServiceMsg)); 106 | count++; 107 | }); 108 | ws.on('message', function(data) { 109 | let response = JSON.parse(data); 110 | 111 | if (count === testData.opCount) { 112 | assert.deepStrictEqual(response.level, testData.finalStatus); 113 | ws.close(); 114 | done(); 115 | } 116 | if (count === 1) { 117 | ws.send(JSON.stringify(testData.unadvertiseServiceMsg)); 118 | count++; 119 | } 120 | }); 121 | }); 122 | }); 123 | }; 124 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-unadvertise.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const WebSocket = require('ws'); 19 | 20 | module.exports = function() { 21 | let testCasesData = [ 22 | { 23 | title: 'unadvertise positive case 1', 24 | advertiseMsg: {op: 'advertise', id: 'advertise_setup1', topic: 'unadvertise_topic1', type: 'std_msgs/String'}, 25 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id1', topic: 'unadvertise_topic1'}, 26 | opCount: 2, 27 | finalStatus: 'none' 28 | }, 29 | { 30 | title: 'unadvertise positive case 2', 31 | advertiseMsg: {op: 'advertise', id: 'advertise_setup2', topic: 'unadvertise_topic2', type: 'std_msgs/String'}, 32 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id2', topic: 'unadvertise_topic2'}, 33 | opCount: 2, 34 | finalStatus: 'none' 35 | }, 36 | { 37 | title: 'unadvertise positive case 3: ROS2 message type format', 38 | advertiseMsg: {op: 'advertise', id: 'advertise_ros2_msg_setup', topic: 'unadvertise_ros2_msg_topic', 39 | type: 'std_msgs/msg/String'}, 40 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_ros2_msg_setup', topic: 'unadvertise_ros2_msg_topic'}, 41 | opCount: 2, 42 | finalStatus: 'none' 43 | }, 44 | { 45 | title: 'unadvertise negative case 1', 46 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id3', topic: 'unadvertise_topic3'}, 47 | opCount: 1, 48 | finalStatus: 'warning' 49 | }, 50 | { 51 | title: 'unadvertise field checking case 1: invalid topic', 52 | advertiseMsg: {op: 'advertise', id: 'advertise_setup4', topic: 'unadvertise_topic4', type: 'std_msgs/String'}, 53 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id4', topic: true}, 54 | opCount: 2, 55 | finalStatus: 'error' 56 | }, 57 | { 58 | title: 'unadvertise field checking case 2: topic cannot be empty', 59 | advertiseMsg: {op: 'advertise', id: 'advertise_setup5', topic: 'unadvertise_topic5', type: 'std_msgs/String'}, 60 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id5', topic: ''}, 61 | opCount: 2, 62 | finalStatus: 'error' 63 | }, 64 | { 65 | title: 'unadvertise field checking case 3: topic cannot contain single quote', 66 | advertiseMsg: {op: 'advertise', id: 'advertise_setup6', topic: 'unadvertise_topic6', type: 'std_msgs/String'}, 67 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id6', topic: "'single_advertise_topic'"}, 68 | opCount: 2, 69 | finalStatus: 'error' 70 | }, 71 | { 72 | title: 'unadvertise field checking case 3: topic cannot contain double quotes', 73 | advertiseMsg: {op: 'advertise', id: 'advertise_setup7', topic: 'unadvertise_topic7', type: 'std_msgs/String'}, 74 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id7', topic: '"double_advertise_topic"'}, 75 | opCount: 2, 76 | finalStatus: 'error' 77 | }, 78 | { 79 | title: 'unadvertise field checking case 3: topic does not support unicode', 80 | advertiseMsg: {op: 'advertise', id: 'advertise_setup8', topic: 'unadvertise_topic8', type: 'std_msgs/String'}, 81 | unadvertiseMsg: {op: 'unadvertise', id: 'unadvertise_id8', topic: '\u8bdd\u9898'}, 82 | opCount: 2, 83 | finalStatus: 'error' 84 | } 85 | ]; 86 | 87 | testCasesData.forEach((testData, index) => { 88 | it(testData.title, function() { 89 | return new Promise((resolve, reject) => { 90 | let ws = new WebSocket('ws://127.0.0.1:9090'); 91 | let counter = 0; 92 | 93 | ws.on('open', function() { 94 | if (testData.advertiseMsg !== undefined) { 95 | ws.send(JSON.stringify(testData.advertiseMsg)); 96 | } else { 97 | ws.send(JSON.stringify(testData.unadvertiseMsg)); 98 | } 99 | counter++; 100 | }); 101 | ws.on('message', function(data) { 102 | if (counter === testData.opCount) { 103 | let response = JSON.parse(data); 104 | assert.deepStrictEqual(response.level, testData.finalStatus); 105 | counter++; 106 | ws.close(); 107 | resolve(); 108 | } 109 | if (counter === 1) { 110 | ws.send(JSON.stringify(testData.unadvertiseMsg)); 111 | counter++; 112 | } 113 | }); 114 | }); 115 | }); 116 | }); 117 | }; 118 | -------------------------------------------------------------------------------- /test/nodejs/protocol/test-unsubscribe.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | /* eslint-disable camelcase */ 18 | 19 | const assert = require('assert'); 20 | const WebSocket = require('ws'); 21 | 22 | module.exports = function() { 23 | let testCasesData = [ 24 | { 25 | title: 'unsubscribe positive case 1', 26 | subscribeMsg1: {op: 'subscribe', id: 'subscribe_setup_id1', topic: 'unsubscribe_topic1', type: 'std_msgs/String'}, 27 | unsubscribeMsg: {op: 'unsubscribe', id: 'subscribe_setup_id1', topic: 'unsubscribe_topic1'}, 28 | opCount: 2, 29 | finalStatus: 'none' 30 | }, 31 | { 32 | title: 'unsubscribe positive case 2', 33 | subscribeMsg1: {op: 'subscribe', id: 'subscribe_setup_id2', topic: 'unsubscribe_topic2', type: 'std_msgs/Bool'}, 34 | subscribeMsg2: {op: 'subscribe', id: 'subscribe_setup_id3', topic: 'unsubscribe_topic2', type: 'std_msgs/Bool'}, 35 | unsubscribeMsg: {op: 'unsubscribe', topic: 'unsubscribe_topic2'}, 36 | opCount: 3, 37 | finalStatus: 'none' 38 | }, 39 | { 40 | title: 'unbscribe negative case 1: unknown topic', 41 | subscribeMsg1: {op: 'subscribe', id: 'subscribe_setup_id4', topic: 'unsubscribe_topic4', type: 'std_msgs/Byte'}, 42 | unsubscribeMsg: {op: 'unsubscribe', id: 'subscribe_setup_id4', topic: 'unsubscribe_topic4x'}, 43 | opCount: 2, 44 | finalStatus: 'warning' 45 | }, 46 | { 47 | title: 'unsubscribe negative case 2: unknown id', 48 | subscribeMsg1: {op: 'subscribe', id: 'subscribe_setup_id5', topic: 'unsubscribe_topic5', type: 'std_msgs/Char'}, 49 | unsubscribeMsg: {op: 'unsubscribe', id: 'subscribe_setup_id5x', topic: 'unsubscribe_topic5'}, 50 | opCount: 2, 51 | finalStatus: 'none' 52 | }, 53 | { 54 | title: 'unsubscribe field checking case 1: invalid id', 55 | subscribeMsg1: {op: 'subscribe', id: 'subscribe_setup_id6', topic: 'subscribe_topic6', type: 'std_msgs/Header'}, 56 | unsubscribeMsg: {op: 'unsubscribe', id: 42, topic: 'subscribe_topic6'}, 57 | opCount: 2, 58 | finalStatus: 'none' 59 | }, 60 | { 61 | title: 'unsubscribe field checking case 2: invalid topic', 62 | subscribeMsg1: { 63 | op: 'subscribe', id: 'subscribe_setup_id7', topic: 'subscribe_topic7', type: 'std_msgs/ColorRGBA'}, 64 | unsubscribeMsg: {op: 'unsubscribe', id: 'subscribe_setup_id7', topic: 42}, 65 | opCount: 2, 66 | finalStatus: 'error' 67 | } 68 | ]; 69 | 70 | testCasesData.forEach((testData, index) => { 71 | it(testData.title, function() { 72 | return new Promise((resolve, reject) => { 73 | let ws = new WebSocket('ws://127.0.0.1:9090'); 74 | let counter = 0; 75 | 76 | ws.on('open', function() { 77 | ws.send(JSON.stringify(testData.subscribeMsg1)); 78 | counter++; 79 | }); 80 | ws.on('message', function(data) { 81 | let response = JSON.parse(data); 82 | 83 | if (counter === testData.opCount) { 84 | assert.deepStrictEqual(response.level, testData.finalStatus); 85 | ws.close(); 86 | resolve(); 87 | } 88 | if (counter === 2 && testData.opCount > 2) { 89 | ws.send(JSON.stringify(testData.unsubscribeMsg)); 90 | counter++; 91 | } 92 | if (counter === 1) { 93 | if (testData.opCount === 2) { 94 | ws.send(JSON.stringify(testData.unsubscribeMsg)); 95 | } else if (testData.opCount === 3) { 96 | ws.send(JSON.stringify(testData.subscribeMsg2)); 97 | } 98 | counter++; 99 | } 100 | }); 101 | }); 102 | }); 103 | }); 104 | }; 105 | -------------------------------------------------------------------------------- /test/nodejs/test-rosauth-internal.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Intel Corporation. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const assert = require('assert'); 18 | const rosauth = require('../../lib/rosauth.js'); 19 | const {sha512} = require('js-sha512'); 20 | 21 | function getJavaScriptTime() { 22 | const t = new Date().getTime(); 23 | return {sec: Math.floor(t / 1000), nanosec: (t % 1000) * 1000 * 1000}; 24 | } 25 | 26 | describe('Test rosauth module internally/directly', function() { 27 | this.timeout(60 * 1000); 28 | 29 | before(function() { 30 | rosauth.setSecretFile('../data/example.secret'); 31 | }); 32 | 33 | after(function() { 34 | }); 35 | 36 | it('Test authenticate() method directly, correct MAC', function() { 37 | const t = getJavaScriptTime(); 38 | let msg = { 39 | mac: '', 40 | client: '192.168.1.101', 41 | dest: '192.168.1.111', 42 | rand: 'xyzabc', 43 | t: {sec: t.sec, nanosec: t.nanosec}, 44 | level: 'admin', 45 | end: {sec: t.sec + 120, nanosec: t.nanosec}, 46 | }; 47 | 48 | msg.mac = sha512('relaxthisain\'tmyfirstrodeo' + 49 | msg.client + msg.dest + msg.rand + msg.t.sec + msg.level + msg.end.sec); 50 | 51 | assert.ok(rosauth.authenticate(msg), 'Should return true for correct MAC'); 52 | }); 53 | 54 | it('Test authenticate() method directly, no MAC', function() { 55 | const t = getJavaScriptTime(); 56 | let msg = { 57 | mac: '', 58 | client: '192.168.1.101', 59 | dest: '192.168.1.111', 60 | rand: 'xyzabc', 61 | t: {sec: t.sec, nanosec: t.nanosec}, 62 | level: 'admin', 63 | end: {sec: t.sec + 120, nanosec: t.nanosec}, 64 | }; 65 | 66 | assert(!rosauth.authenticate(msg), 'Should NOT return true for incorrect MAC'); 67 | }); 68 | 69 | it('Test authenticate() method directly, wrong MAC', function() { 70 | const t = getJavaScriptTime(); 71 | let msg = { 72 | mac: '', 73 | client: '192.168.1.101', 74 | dest: '192.168.1.111', 75 | rand: 'xyzabc', 76 | t: {sec: t.sec, nanosec: t.nanosec}, 77 | level: 'admin', 78 | end: {sec: t.sec + 120, nanosec: t.nanosec}, 79 | }; 80 | 81 | msg.mac = sha512('relaxthisain\'tmyfirstrodeo--' + 82 | msg.client + msg.dest + msg.rand + msg.t.sec + msg.level + msg.end.sec); 83 | 84 | assert(!rosauth.authenticate(msg), 'Should NOT return true for incorrect MAC'); 85 | }); 86 | 87 | [ 88 | {secDelta: -Number.MAX_VALUE, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 89 | {secDelta: -Number.MAX_SAFE_INTEGER, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 90 | {secDelta: -6, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 91 | {secDelta: -7, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 92 | {secDelta: -5, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 93 | 94 | {secDelta: 5, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 95 | {secDelta: 6, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 96 | {secDelta: 7, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 97 | {secDelta: Number.MAX_SAFE_INTEGER, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 98 | {secDelta: Number.MAX_VALUE, nanosecDelta: 0, secDeltaEnd: 120, nanosecDeltaEnd: 0}, 99 | 100 | {secDelta: Number.NaN, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: 0}, 101 | {secDelta: 0, nanosecDelta: Number.NaN, secDeltaEnd: 0, nanosecDeltaEnd: 0}, 102 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: Number.NaN, nanosecDeltaEnd: 0}, 103 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: Number.NaN}, 104 | 105 | {secDelta: 0, nanosecDelta: 1000 * 1000 * 1000, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 106 | {secDelta: 0, nanosecDelta: Number.MAX_VALUE, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 107 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 1000 * 1000 * 1000}, 108 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: Number.MAX_VALUE}, 109 | 110 | {secDelta: -getJavaScriptTime().sec - 10, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 111 | {secDelta: -Number.MAX_VALUE, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 112 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -Number.MAX_VALUE, nanosecDeltaEnd: 0}, 113 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -getJavaScriptTime().sec - 10, nanosecDeltaEnd: 0}, 114 | 115 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -0, nanosecDeltaEnd: 0}, 116 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -1, nanosecDeltaEnd: 0}, 117 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -2, nanosecDeltaEnd: 0}, 118 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -3, nanosecDeltaEnd: 0}, 119 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -4, nanosecDeltaEnd: 0}, 120 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -100, nanosecDeltaEnd: 0}, 121 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -100000, nanosecDeltaEnd: 0}, 122 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -Number.MAX_SAFE_INTEGER, nanosecDeltaEnd: 0}, 123 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: -Number.MAX_VALUE, nanosecDeltaEnd: 0}, 124 | 125 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: -0}, 126 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: -1}, 127 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: -2}, 128 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: -1000}, 129 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: -2000}, 130 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 0, nanosecDeltaEnd: -3000}, 131 | ].forEach((testData, index) => { 132 | it('Test authenticate() method directly, correct MAC, wrong timestamp, case#' + index, function() { 133 | const t = getJavaScriptTime(); 134 | let msg = { 135 | mac: '', 136 | client: '192.168.1.101', 137 | dest: '192.168.1.111', 138 | rand: 'xyzabc', 139 | t: {sec: t.sec + testData.secDelta, nanosec: t.nanosec + testData.nanosecDelta}, 140 | level: 'admin', 141 | end: {sec: t.sec + testData.secDeltaEnd, nanosec: t.nanosec + testData.nanosecDeltaEnd}, 142 | }; 143 | 144 | msg.mac = sha512('relaxthisain\'tmyfirstrodeo' + 145 | msg.client + msg.dest + msg.rand + msg.t.sec + msg.level + msg.end.sec); 146 | 147 | assert(!rosauth.authenticate(msg), 'Should return false for correct MAC + wrong timestamp'); 148 | }); 149 | }); 150 | 151 | [ 152 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 153 | {secDelta: -1, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 154 | {secDelta: -2, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 155 | {secDelta: -3, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 156 | {secDelta: -4, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 157 | 158 | {secDelta: 1, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 159 | {secDelta: 2, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 160 | {secDelta: 3, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 161 | {secDelta: 4, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 162 | 163 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 5, nanosecDeltaEnd: 0}, 164 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: 10, nanosecDeltaEnd: 0}, 165 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: Number.MAX_SAFE_INTEGER, nanosecDeltaEnd: 0}, 166 | {secDelta: 0, nanosecDelta: 0, secDeltaEnd: Number.MAX_VALUE, nanosecDeltaEnd: 0}, 167 | ].forEach((testData, index) => { 168 | it('Test authenticate() method directly, correct MAC, correct timestamp, case#' + index, function() { 169 | const t = getJavaScriptTime(); 170 | let msg = { 171 | mac: '', 172 | client: '192.168.1.101', 173 | dest: '192.168.1.111', 174 | rand: 'xyzabc', 175 | t: {sec: t.sec, nanosec: t.nanosec}, 176 | level: 'admin', 177 | end: {sec: t.sec + 120, nanosec: t.nanosec}, 178 | }; 179 | 180 | msg.mac = sha512('relaxthisain\'tmyfirstrodeo' + 181 | msg.client + msg.dest + msg.rand + msg.t.sec + msg.level + msg.end.sec); 182 | 183 | assert.ok(rosauth.authenticate(msg), 'Should return true for correct MAC'); 184 | }); 185 | }); 186 | 187 | }); 188 | --------------------------------------------------------------------------------