├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ └── main.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── AUTHORS.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── eslint.config.js ├── examples ├── action_client.html ├── action_server.html ├── math.html ├── node_simple.js ├── react-example │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── App.test.jsx │ │ ├── component_examples │ │ └── example_functions.jsx │ │ ├── index.css │ │ ├── index.jsx │ │ ├── logo.svg │ │ └── setupTests.js ├── ros2_action_client.html ├── ros2_action_server.html ├── ros2_simple.html ├── simple.html ├── tf.html └── urdf.html ├── jsdoc_conf.json ├── package-lock.json ├── package.json ├── src ├── RosLib.js ├── actionlib │ ├── ActionClient.js │ ├── ActionListener.js │ ├── Goal.js │ ├── SimpleActionServer.js │ └── index.js ├── core │ ├── Action.js │ ├── GoalStatus.ts │ ├── Message.js │ ├── Param.js │ ├── Ros.js │ ├── Service.js │ ├── SocketAdapter.js │ ├── Topic.js │ └── index.js ├── math │ ├── Pose.js │ ├── Quaternion.js │ ├── Transform.js │ ├── Vector3.js │ └── index.js ├── tf │ ├── ROS2TFClient.js │ ├── TFClient.js │ └── index.js ├── urdf │ ├── UrdfBox.js │ ├── UrdfColor.js │ ├── UrdfCylinder.js │ ├── UrdfJoint.js │ ├── UrdfLink.js │ ├── UrdfMaterial.js │ ├── UrdfMesh.js │ ├── UrdfModel.js │ ├── UrdfSphere.js │ ├── UrdfTypes.js │ ├── UrdfVisual.js │ └── index.js └── util │ ├── cborTypedArrayTags.js │ ├── decompressPng.js │ └── shim │ └── decompressPng.js ├── test ├── build.bash ├── cbor.test.js ├── cdn-import.test.js ├── examples │ ├── check-topics.example.js │ ├── fibonacci.example.js │ ├── params.examples.js │ ├── pubsub.example.js │ ├── setup_examples.bash │ ├── setup_examples.launch │ ├── tf.example.js │ ├── tf_service.example.js │ └── topic-listener.example.js ├── math-examples.test.js ├── quaternion.test.js ├── service.test.js ├── tfclient.test.js ├── transform.test.js └── urdf.test.js ├── tsconfig.json └── vite.config.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | ignore: 8 | - dependency-name: "*" 9 | update-types: ["version-update:semver-patch"] 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs Deployment 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | docs: 9 | name: Docs Deployment 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | cache: npm 16 | node-version: 20 17 | - name: Install 18 | run: npm ci 19 | - name: Generate docs 20 | run: npm run doc 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v4 23 | with: 24 | github_token: ${{ secrets.RWT_BOT_PAT }} 25 | publish_dir: doc 26 | destination_dir: . 27 | enable_jekyll: false 28 | force_orphan: false # So we keep the doc history 29 | commit_message: JSDoc ${{ github.event.release.name }} 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | defaults: 6 | run: 7 | shell: bash 8 | 9 | jobs: 10 | ci: 11 | name: ${{ matrix.ros_distro }} (node ${{ matrix.node_version }}) 12 | if: ${{ github.actor != 'RWT-bot' }} 13 | runs-on: ubuntu-latest 14 | container: 15 | image: ros:${{ matrix.ros_distro }}-ros-core 16 | options: --cap-add=SYS_ADMIN 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | ros_distro: [noetic] 21 | node_version: [18, 20] 22 | env: 23 | ROS_DISTRO: ${{ matrix.ros_distro }} 24 | steps: 25 | - name: Install git in container 26 | run: | 27 | apt-get update 28 | apt-get install -q -y git 29 | - name: Set git safe directory 30 | run: | 31 | git config --global safe.directory '*' 32 | - uses: actions/checkout@v4 33 | env: 34 | TOKEN: "${{ github.event_name == 'push' && endsWith(github.ref, 'develop') && matrix.ros_distro == 'noetic' && secrets.RWT_BOT_PAT || github.token }}" 35 | with: 36 | token: ${{ env.TOKEN }} 37 | - uses: actions/setup-node@v4 38 | with: 39 | cache: npm 40 | node-version: ${{ matrix.node_version }} 41 | - name: Own /github/home and PWD 42 | run: | 43 | chown -hR 1001:121 /github/home . 44 | - name: Install apt dependencies 45 | run: | 46 | apt-get install -q -y libasound2 libnss3 ros-$ROS_DISTRO-rosbridge-server ros-$ROS_DISTRO-tf2-web-republisher ros-$ROS_DISTRO-common-tutorials ros-$ROS_DISTRO-rospy-tutorials ros-$ROS_DISTRO-actionlib-tutorials 47 | - name: Tests 48 | run: | 49 | bash -c "source /opt/ros/$ROS_DISTRO/setup.bash && bash test/build.bash" 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vagrant 3 | doc 4 | node_modules 5 | *.out 6 | *.log 7 | *.tar.gz 8 | dist 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.md 2 | .github 3 | .gitignore 4 | .eslint.config.js 5 | Gruntfile.js 6 | bower.json 7 | examples 8 | jsdoc_conf.json 9 | test 10 | tsconfig.json 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | unsafe-perm = true -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Original Authors 2 | ---------------- 3 | 4 | * [Russell Toris](http://users.wpi.edu/~rctoris/) (rctoris@wpi.edu) 5 | * Jihoon Lee (jihoonlee.in@gmail.com) 6 | * Brandon Alexander (balexander@willowgarage.com) 7 | * David Gossow (dgossow@willowgarage.com) 8 | * Benjamin Pitzer (ben.pitzer@gmail.com) 9 | 10 | Contributors 11 | ------------ 12 | 13 | * [Matthijs van der Burgh](https://github.com/MatthijsBurgh) (MatthijsBurgh@outlook.com) 14 | * Graeme Yeates (yeatesgraeme@gmail.com) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2014, Worcester Polytechnic Institute, Robert Bosch 4 | LLC, Yujin Robot. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | * Neither the name of Worcester Polytechnic Institute, Robert 17 | Bosch LLC, Yujin Robot nor the names of its contributors may be 18 | used to endorse or promote products derived from this software 19 | without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roslibjs 2 | 3 | [![CI](https://github.com/RobotWebTools/roslibjs/actions/workflows/main.yml/badge.svg)](https://github.com/RobotWebTools/roslibjs/actions/workflows/main.yml) 4 | 5 | ## The Standard ROS JavaScript Library 6 | 7 | For full documentation see the [ROS wiki](http://wiki.ros.org/roslibjs). 8 | 9 | [JSDoc](https://robotwebtools.github.io/roslibjs) can be found on the Robot Web Tools website. 10 | 11 | This project is released as part of the [Robot Web Tools](https://robotwebtools.github.io/) effort. 12 | 13 | ## Usage 14 | 15 | Install roslibjs with any NPM-compatible package manager via, for example, 16 | 17 | ```bash 18 | npm install roslib 19 | ``` 20 | 21 | ~Pre-built files can be found in either [roslib.js](build/roslib.js) or [roslib.min.js](build/roslib.min.js).~ 22 | 23 | As we are updating to v2, we don't provide pre-built files anymore in the repo. 24 | 25 | Alternatively, you can use the v1 release via the [JsDelivr](https://www.jsdelivr.com/) CDN: ([full](https://cdn.jsdelivr.net/npm/roslib@1/build/roslib.js)) | ([min](https://cdn.jsdelivr.net/npm/roslib@1/build/roslib.min.js)) 26 | 27 | ## Troubleshooting 28 | 29 | 1. Check that connection is established. You can listen to error and 30 | connection events to report them to console. See 31 | examples/simple.html for a complete example: 32 | 33 | ```js 34 | ros.on('error', function(error) { console.log( error ); }); 35 | ros.on('connection', function() { console.log('Connection made!'); }); 36 | ``` 37 | 38 | 2. Check that you have the websocket server is running on 39 | port 9090. Something like this should do: 40 | 41 | ```bash 42 | netstat -a | grep 9090 43 | ``` 44 | 45 | ## Dependencies 46 | 47 | roslibjs has a number of dependencies. You will need to run: 48 | 49 | ```bash 50 | npm install 51 | ``` 52 | 53 | Depending on your build environment. 54 | 55 | ## Build 56 | 57 | Checkout [CONTRIBUTING.md](CONTRIBUTING.md) for details on building. 58 | 59 | ## License 60 | 61 | roslibjs is released with a BSD license. For full terms and conditions, see the [LICENSE](LICENSE) file. 62 | 63 | ## Authors 64 | 65 | See the [AUTHORS.md](AUTHORS.md) file for a full list of contributors. 66 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Robot Web Tools Module Release TODOs 2 | 3 | This document describes TODOs and checklists in order to release 4 | Robot Web Tool javascript modules([roslibjs](https://github.com/RobotWebTools/roslibjs), [ros2djs](https://github.com/RobotWebTools/ros2djs), [ros3djs](https://github.com/RobotWebTools/ros3djs)). 5 | 6 | ## 0. Make sure that the releasing module is compatible with other RWT modules 7 | 8 | ## 1. Generate CHANGELOG using [github-changelog-generator](https://github.com/github-changelog-generator/github-changelog-generator) 9 | 10 | ```bash 11 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u robotwebtools -p --usernames-as-github-logins --simple-list --no-issues --date-format "%Y-%m-%d %H:%M %z" -t 12 | ``` 13 | 14 | ## 2. Bump a new version 15 | 16 | * Version bump in package.json, bower.json, and in the main file. e.g) [RosLib.js](src/RosLib.js) 17 | * Tag the version 18 | 19 | ## 3. Release modules 20 | 21 | ### NPM 22 | 23 | Publish the module. We publish in the global scope. 24 | 25 | * `npm publish` 26 | 27 | ### CDN 28 | 29 | Hosted via the [JsDelivr](https://www.jsdelivr.com/) CDN, which takes it directly from the repo. 30 | 31 | ## 4. Create GitHub Release 32 | 33 | * Create a new GitHub release based on the new git tag. 34 | * Add the version number as release title (Without leading `v`). 35 | * Let GitHub auto-generate the Changelog 36 | * Mark `Set as latest release` 37 | * Publish release 38 | 39 | ## 5. Update JSdocs in Robot Web Tools website 40 | 41 | The JSdocs are update automatically by GitHub Actions [config](.github/workflows/docs.yml). The GitHub release created above, will trigger this run. The docs are hosted in their own repository at the `gh-pages` branch. 42 | 43 | ## 6. Sync `develop` branch with `master` 44 | 45 | `Master` branch should represent the latest release. 46 | 47 | * Create a PR against `master` from `develop` 48 | * Do *Rebase and merge* to have the same history as `develop` branch 49 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | 3 | export default [ 4 | { 5 | languageOptions: { 6 | 'globals': { 7 | ...globals.browser, 8 | ...globals.node, 9 | 'bson': true 10 | }, 11 | 'parserOptions': { 12 | 'ecmaFeatures': { 13 | 'jsx': true 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | ignores: ['dist'] 20 | }, 21 | { 22 | rules: { 23 | curly: 2, 24 | eqeqeq: 2, 25 | 'wrap-iife': [2, 'any'], 26 | 'no-use-before-define': 0, 27 | 'no-caller': 2, 28 | 'dot-notation': 0, 29 | 'no-undef': 2, 30 | 'no-cond-assign': 0, 31 | 'no-eq-null': 0, 32 | strict: 0, 33 | quotes: [2, 'single'], 34 | 'no-proto': 2, 35 | 'linebreak-style': 2, 36 | 'key-spacing': [2, {afterColon: true}] 37 | }, 38 | files: ['**/*.{js,jsx,cjs}'] 39 | } 40 | ]; 41 | -------------------------------------------------------------------------------- /examples/action_client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 68 | 69 | 70 | 71 |

Fibonacci ActionClient Example

72 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 73 | console for the output.

74 |
    75 |
  1. roscore
  2. 76 |
  3. rosrun actionlib_tutorials fibonacci_server
  4. 77 |
  5. roslaunch rosbridge_server rosbridge_websocket.launch
  6. 78 |
79 |
80 |

81 | Connecting to rosbridge... 82 |

83 | 86 | 89 | 92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /examples/action_server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 62 | 63 | 64 | 65 |

Fibonacci ActionClient Example

66 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 67 | console for the output.

68 |
    69 |
  1. roscore
  2. 70 |
  3. roslaunch rosbridge_server rosbridge_websocket.launch
  4. 71 |
  5. refresh this page
  6. 72 |
  7. rosrun actionlib_tutorials fibonacci_client
  8. 73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /examples/math.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 46 | 47 | 48 | 49 |

Simple Math Example

50 |

Check the JavaScript console for the output.

51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/node_simple.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Connecting to ROS 4 | import ROSLIB from 'roslib'; 5 | 6 | var ros = new ROSLIB.Ros({ 7 | url: 'ws://localhost:9090' 8 | }); 9 | 10 | ros.on('connection', function() { 11 | console.log('Connected to websocket server.'); 12 | }); 13 | 14 | ros.on('error', function(error) { 15 | console.log('Error connecting to websocket server: ', error); 16 | }); 17 | 18 | ros.on('close', function() { 19 | console.log('Connection to websocket server closed.'); 20 | }); 21 | 22 | // Publishing a Topic 23 | // ------------------ 24 | 25 | var cmdVel = new ROSLIB.Topic({ 26 | ros: ros, 27 | name: '/cmd_vel', 28 | messageType: 'geometry_msgs/Twist' 29 | }); 30 | 31 | var twist = { 32 | linear: { 33 | x: 0.1, 34 | y: 0.2, 35 | z: 0.3 36 | }, 37 | angular: { 38 | x: -0.1, 39 | y: -0.2, 40 | z: -0.3 41 | } 42 | }; 43 | 44 | console.log('Publishing cmd_vel'); 45 | cmdVel.publish(twist); 46 | -------------------------------------------------------------------------------- /examples/react-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/react-example/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /examples/react-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-examples", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-scripts": "5.0.1", 11 | "roslib": "^1.3.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": [ 21 | "react-app", 22 | "react-app/jest" 23 | ] 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/react-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/roslibjs/c78d623856928915a37fa517f111014116d83d76/examples/react-example/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/react-example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/roslibjs/c78d623856928915a37fa517f111014116d83d76/examples/react-example/public/logo192.png -------------------------------------------------------------------------------- /examples/react-example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/roslibjs/c78d623856928915a37fa517f111014116d83d76/examples/react-example/public/logo512.png -------------------------------------------------------------------------------- /examples/react-example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/react-example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 50vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | @keyframes App-logo-spin { 28 | from { 29 | transform: rotate(0deg); 30 | } 31 | to { 32 | transform: rotate(360deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/react-example/src/App.jsx: -------------------------------------------------------------------------------- 1 | import logo from './logo.svg' 2 | import './App.css' 3 | import SendMessage from './component_examples/example_functions' 4 | import React from 'react'; 5 | 6 | function App() { 7 | return ( 8 |
9 |
10 | logo 11 |
12 | 13 | 14 |
15 | ) 16 | } 17 | 18 | export default App 19 | -------------------------------------------------------------------------------- /examples/react-example/src/App.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import App from './App' 3 | import React from 'react'; 4 | import { expect, test } from 'vitest'; 5 | 6 | test('renders learn react link', () => { 7 | render() 8 | expect(screen.getByText(/Send a message to turtle/i)).toBeTruthy(); 9 | }) 10 | -------------------------------------------------------------------------------- /examples/react-example/src/component_examples/example_functions.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import * as ROSLIB from '../../../../src/RosLib.js' 3 | 4 | function SendMessage() { 5 | const [status, setStatus] = useState('Not connected') 6 | const [linear, setLinear] = useState({ x: 0, y: 0, z: 0 }) 7 | const [angular, setAngular] = useState({ x: 0, y: 0, z: 0 }) 8 | const ros = new ROSLIB.Ros({encoding: 'ascii'}) 9 | 10 | function convert(input){ 11 | if (input.charAt(0) === '-') { 12 | let x = input.slice(0) 13 | return parseInt(x) 14 | } else { 15 | return parseInt(input) 16 | } 17 | } 18 | 19 | function connect() { 20 | ros.connect('ws://localhost:9090') 21 | // won't let the user connect more than once 22 | ros.on('error', function (error) { 23 | console.log(error) 24 | setStatus(error) 25 | }) 26 | 27 | // Find out exactly when we made a connection. 28 | ros.on('connection', function () { 29 | console.log('Connected!') 30 | setStatus('Connected!') 31 | }) 32 | 33 | ros.on('close', function () { 34 | console.log('Connection closed') 35 | setStatus('Connection closed') 36 | }) 37 | } 38 | 39 | function publish() { 40 | if (status !== 'Connected!') { 41 | connect() 42 | } 43 | const cmdVel = new ROSLIB.Topic({ 44 | ros: ros, 45 | name: 'pose_topic', 46 | messageType: 'geometry_msgs/Pose2D' 47 | }) 48 | 49 | const data = { 50 | x: linear.x, 51 | y: linear.y, 52 | theta: angular.z 53 | } 54 | 55 | // publishes to the queue 56 | console.log('msg', data) 57 | cmdVel.publish(data) 58 | } 59 | 60 | return ( 61 |
62 |
63 | {status} 64 |
65 |

Send a message to turtle

66 |

Linear:

67 | 68 | setLinear({...linear, x: convert(ev.target.value)})}/> 69 | 70 | setLinear({...linear, y: convert(ev.target.value)})}/> 71 | 72 | setLinear({...linear, z: convert(ev.target.value)})}/> 73 |

Angular:

74 | 75 | setAngular({...angular, x: convert(ev.target.value)})}/> 76 | 77 | setAngular({...angular, y: convert(ev.target.value)})}/> 78 | 79 | setAngular({...angular, z: convert(ev.target.value)})}/> 80 |
81 | 82 |
83 |
84 | ) 85 | } 86 | 87 | export default SendMessage 88 | -------------------------------------------------------------------------------- /examples/react-example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-example/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import './index.css' 4 | import App from './App' 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')) 7 | root.render( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /examples/react-example/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-example/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /examples/ros2_action_client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 61 | 62 | 63 | 64 |

Fibonacci ActionClient Example

65 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 66 | console for the output.

67 |
    68 |
  1. ros2 launch rosbridge_server rosbridge_websocket_launch.xml
  2. 69 |
  3. ros2 run action_tutorials_py fibonacci_action_server
  4. 70 |
71 |
72 |

73 | Connecting to rosbridge... 74 |

75 | 78 | 81 | 84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /examples/ros2_action_server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 62 | 63 | 64 | 65 |

Fibonacci ActionServer Example

66 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 67 | console for the output.

68 |
    69 |
  1. ros2 launch rosbridge_server rosbridge_websocket_launch.xml
  2. 70 |
  3. refresh this page
  4. 71 |
  5. ros2 run action_tutorials_py fibonacci_action_client 72 |
    or
    73 | ros2 action send_goal --feedback /fibonacci action_tutorials_interfaces/action/Fibonacci order:\ 20\ 74 |
  6. 75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/ros2_simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 146 | 147 | 148 | 149 |

Simple roslib Example

150 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 151 | console for the output.

152 |
    153 |
  1. ros2 topic pub /listener std_msgs/msg/String "{ data: 'hello world' }"
  2. 154 |
  3. ros2 topic echo /cmd_vel
  4. 155 |
  5. ros2 run demo_nodes_py add_two_ints_server
  6. 156 |
  7. ros2 launch rosbridge_server rosbridge_websocket_launch.xml
  8. 157 |
158 |
159 |

160 | Connecting to rosbridge... 161 |

162 | 165 | 168 | 171 |
172 | 173 | 174 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 161 | 162 | 163 | 164 |

Simple roslib Example

165 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 166 | console for the output.

167 |
    168 |
  1. roscore
  2. 169 |
  3. rostopic pub /listener std_msgs/String "Hello, World"
  4. 170 |
  5. rostopic echo /cmd_vel
  6. 171 |
  7. rosrun rospy_tutorials add_two_ints_server
  8. 172 |
  9. roslaunch rosbridge_server rosbridge_websocket.launch
  10. 173 |
174 |
175 |

176 | Connecting to rosbridge... 177 |

178 | 181 | 184 | 187 |
188 | 189 | 190 | -------------------------------------------------------------------------------- /examples/tf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 60 | 61 | 62 | 63 |

Simple TF Example

64 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 65 | console for the output.

66 |
    67 |
  1. roslaunch turtle_tf turtle_tf_demo.launch 68 |
  2. 69 |
  3. rosrun tf2_web_republisher tf2_web_republisher 70 |
  4. 71 |
  5. roslaunch rosbridge_server rosbridge_websocket.launch 72 |
  6. 73 |
  7. Use your arrow keys on your keyboard to move the turtle (must have turtle_tf_demo.launch 74 | terminal focused).
  8. 75 |
76 |
77 |

78 | Connecting to rosbridge... 79 |

80 | 83 | 86 | 89 |
90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/urdf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 58 | 59 | 60 | 61 |

Simple URDF Parsing Example

62 | 63 |

Run the following commands in the terminal then refresh this page. Check the JavaScript 64 | console for the output.

65 |
    66 |
  1. roslaunch pr2_description upload_pr2.launch
  2. 67 |
  3. roslaunch rosbridge_server rosbridge_websocket.launch
  4. 68 |
69 |
70 |

71 | Connecting to rosbridge... 72 |

73 | 76 | 79 | 82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /jsdoc_conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "plugins/markdown" ], 3 | "markdown": { 4 | "parser": "gfm" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roslib", 3 | "homepage": "https://robotwebtools.github.io", 4 | "description": "The standard ROS Javascript Library", 5 | "version": "1.4.1", 6 | "license": "BSD-2-Clause", 7 | "files": [ 8 | "dist" 9 | ], 10 | "main": "./dist/RosLib.umd.cjs", 11 | "module": "./dist/RosLib.js", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/RosLib.js", 15 | "require": "./dist/RosLib.umd.cjs" 16 | } 17 | }, 18 | "type": "module", 19 | "devDependencies": { 20 | "@testing-library/react": "^16.0.0", 21 | "@types/node": "^22.0.0", 22 | "@types/ws": "^8.5.10", 23 | "eslint": "^9.0.0", 24 | "globals": "^16.0.0", 25 | "jsdoc": "^4.0.2", 26 | "jsdom": "^26.0.0", 27 | "typescript": "^5.2.2", 28 | "vite": "^6.0.1", 29 | "vite-plugin-checker": "^0.9.1", 30 | "vite-plugin-dts": "^4.0.2", 31 | "vitest": "^3.0.2" 32 | }, 33 | "dependencies": { 34 | "@xmldom/xmldom": "^0.9.0", 35 | "cbor-js": "^0.1.0", 36 | "eventemitter3": "^5.0.1", 37 | "pngparse": "^2.0.0", 38 | "ws": "^8.0.0" 39 | }, 40 | "directories": { 41 | "example": "examples", 42 | "test": "test" 43 | }, 44 | "engines": { 45 | "node": ">=0.10" 46 | }, 47 | "scripts": { 48 | "build": "vite build", 49 | "doc": "jsdoc -r -c jsdoc_conf.json -d ./doc .", 50 | "lint": "eslint .", 51 | "test": "vitest", 52 | "prepublishOnly": "npm run test", 53 | "prepare": "npm run build" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/RobotWebTools/roslibjs/releases" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/RobotWebTools/roslibjs/issues" 61 | }, 62 | "keywords": [ 63 | "ROS", 64 | "ros", 65 | "roslib", 66 | "roslibjs", 67 | "robot" 68 | ], 69 | "types": "dist/RosLib.d.ts", 70 | "author": "Robot Webtools Team (https://robotwebtools.github.io)" 71 | } 72 | -------------------------------------------------------------------------------- /src/RosLib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Russell Toris - rctoris@wpi.edu 4 | */ 5 | 6 | /** @description Library version */ 7 | export const REVISION = '1.4.1'; 8 | export * from './core/index.js'; 9 | export * from './actionlib/index.js'; 10 | export * from './math/index.js'; 11 | export * from './tf/index.js'; 12 | export * from './urdf/index.js'; 13 | 14 | import * as Core from './core/index.js'; 15 | import * as ActionLib from './actionlib/index.js'; 16 | import * as Math from './math/index.js'; 17 | import * as Tf from './tf/index.js'; 18 | import * as Urdf from './urdf/index.js'; 19 | 20 | // Add to global namespace for in-browser support (i.e. CDN) 21 | globalThis.ROSLIB = { 22 | REVISION, 23 | ...Core, 24 | ...ActionLib, 25 | ...Math, 26 | ...Tf, 27 | ...Urdf 28 | }; 29 | -------------------------------------------------------------------------------- /src/actionlib/ActionClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Russell Toris - rctoris@wpi.edu 4 | */ 5 | 6 | import Topic from '../core/Topic.js'; 7 | import Message from '../core/Message.js'; 8 | import Ros from '../core/Ros.js'; 9 | import { EventEmitter } from 'eventemitter3'; 10 | 11 | /** 12 | * An actionlib action client. 13 | * 14 | * Emits the following events: 15 | * * 'timeout' - If a timeout occurred while sending a goal. 16 | * * 'status' - The status messages received from the action server. 17 | * * 'feedback' - The feedback messages received from the action server. 18 | * * 'result' - The result returned from the action server. 19 | * 20 | */ 21 | export default class ActionClient extends EventEmitter { 22 | goals = {}; 23 | /** flag to check if a status has been received */ 24 | receivedStatus = false 25 | /** 26 | * @param {Object} options 27 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 28 | * @param {string} options.serverName - The action server name, like '/fibonacci'. 29 | * @param {string} options.actionName - The action message name, like 'actionlib_tutorials/FibonacciAction'. 30 | * @param {number} [options.timeout] - The timeout length when connecting to the action server. 31 | * @param {boolean} [options.omitFeedback] - The flag to indicate whether to omit the feedback channel or not. 32 | * @param {boolean} [options.omitStatus] - The flag to indicate whether to omit the status channel or not. 33 | * @param {boolean} [options.omitResult] - The flag to indicate whether to omit the result channel or not. 34 | */ 35 | constructor(options) { 36 | super(); 37 | this.ros = options.ros; 38 | this.serverName = options.serverName; 39 | this.actionName = options.actionName; 40 | this.timeout = options.timeout; 41 | this.omitFeedback = options.omitFeedback; 42 | this.omitStatus = options.omitStatus; 43 | this.omitResult = options.omitResult; 44 | 45 | // create the topics associated with actionlib 46 | this.feedbackListener = new Topic({ 47 | ros: this.ros, 48 | name: this.serverName + '/feedback', 49 | messageType: this.actionName + 'Feedback' 50 | }); 51 | 52 | this.statusListener = new Topic({ 53 | ros: this.ros, 54 | name: this.serverName + '/status', 55 | messageType: 'actionlib_msgs/GoalStatusArray' 56 | }); 57 | 58 | this.resultListener = new Topic({ 59 | ros: this.ros, 60 | name: this.serverName + '/result', 61 | messageType: this.actionName + 'Result' 62 | }); 63 | 64 | this.goalTopic = new Topic({ 65 | ros: this.ros, 66 | name: this.serverName + '/goal', 67 | messageType: this.actionName + 'Goal' 68 | }); 69 | 70 | this.cancelTopic = new Topic({ 71 | ros: this.ros, 72 | name: this.serverName + '/cancel', 73 | messageType: 'actionlib_msgs/GoalID' 74 | }); 75 | 76 | // advertise the goal and cancel topics 77 | this.goalTopic.advertise(); 78 | this.cancelTopic.advertise(); 79 | 80 | // subscribe to the status topic 81 | if (!this.omitStatus) { 82 | this.statusListener.subscribe((statusMessage) => { 83 | this.receivedStatus = true; 84 | statusMessage.status_list.forEach((status) => { 85 | var goal = this.goals[status.goal_id.id]; 86 | if (goal) { 87 | goal.emit('status', status); 88 | } 89 | }); 90 | }); 91 | } 92 | 93 | // subscribe the the feedback topic 94 | if (!this.omitFeedback) { 95 | this.feedbackListener.subscribe((feedbackMessage) => { 96 | var goal = this.goals[feedbackMessage.status.goal_id.id]; 97 | if (goal) { 98 | goal.emit('status', feedbackMessage.status); 99 | goal.emit('feedback', feedbackMessage.feedback); 100 | } 101 | }); 102 | } 103 | 104 | // subscribe to the result topic 105 | if (!this.omitResult) { 106 | this.resultListener.subscribe((resultMessage) => { 107 | var goal = this.goals[resultMessage.status.goal_id.id]; 108 | 109 | if (goal) { 110 | goal.emit('status', resultMessage.status); 111 | goal.emit('result', resultMessage.result); 112 | } 113 | }); 114 | } 115 | 116 | // If timeout specified, emit a 'timeout' event if the action server does not respond 117 | if (this.timeout) { 118 | setTimeout(() => { 119 | if (!this.receivedStatus) { 120 | this.emit('timeout'); 121 | } 122 | }, this.timeout); 123 | } 124 | } 125 | /** 126 | * Cancel all goals associated with this ActionClient. 127 | */ 128 | cancel() { 129 | var cancelMessage = {}; 130 | this.cancelTopic.publish(cancelMessage); 131 | } 132 | /** 133 | * Unsubscribe and unadvertise all topics associated with this ActionClient. 134 | */ 135 | dispose() { 136 | this.goalTopic.unadvertise(); 137 | this.cancelTopic.unadvertise(); 138 | if (!this.omitStatus) { 139 | this.statusListener.unsubscribe(); 140 | } 141 | if (!this.omitFeedback) { 142 | this.feedbackListener.unsubscribe(); 143 | } 144 | if (!this.omitResult) { 145 | this.resultListener.unsubscribe(); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/actionlib/ActionListener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Justin Young - justin@oodar.com.au 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import Topic from '../core/Topic.js'; 8 | import Ros from '../core/Ros.js'; 9 | import { EventEmitter } from 'eventemitter3'; 10 | 11 | /** 12 | * An actionlib action listener. 13 | * 14 | * Emits the following events: 15 | * * 'status' - The status messages received from the action server. 16 | * * 'feedback' - The feedback messages received from the action server. 17 | * * 'result' - The result returned from the action server. 18 | * 19 | 20 | */ 21 | export default class ActionListener extends EventEmitter { 22 | /** 23 | * @param {Object} options 24 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 25 | * @param {string} options.serverName - The action server name, like '/fibonacci'. 26 | * @param {string} options.actionName - The action message name, like 'actionlib_tutorials/FibonacciAction'. 27 | */ 28 | constructor(options) { 29 | super(); 30 | this.ros = options.ros; 31 | this.serverName = options.serverName; 32 | this.actionName = options.actionName; 33 | 34 | // create the topics associated with actionlib 35 | var goalListener = new Topic({ 36 | ros: this.ros, 37 | name: this.serverName + '/goal', 38 | messageType: this.actionName + 'Goal' 39 | }); 40 | 41 | var feedbackListener = new Topic({ 42 | ros: this.ros, 43 | name: this.serverName + '/feedback', 44 | messageType: this.actionName + 'Feedback' 45 | }); 46 | 47 | var statusListener = new Topic({ 48 | ros: this.ros, 49 | name: this.serverName + '/status', 50 | messageType: 'actionlib_msgs/GoalStatusArray' 51 | }); 52 | 53 | var resultListener = new Topic({ 54 | ros: this.ros, 55 | name: this.serverName + '/result', 56 | messageType: this.actionName + 'Result' 57 | }); 58 | 59 | goalListener.subscribe((goalMessage) => { 60 | this.emit('goal', goalMessage); 61 | }); 62 | 63 | statusListener.subscribe((statusMessage) => { 64 | statusMessage.status_list.forEach((status) => { 65 | this.emit('status', status); 66 | }); 67 | }); 68 | 69 | feedbackListener.subscribe((feedbackMessage) => { 70 | this.emit('status', feedbackMessage.status); 71 | this.emit('feedback', feedbackMessage.feedback); 72 | }); 73 | 74 | // subscribe to the result topic 75 | resultListener.subscribe((resultMessage) => { 76 | this.emit('status', resultMessage.status); 77 | this.emit('result', resultMessage.result); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/actionlib/Goal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Russell Toris - rctoris@wpi.edu 4 | */ 5 | 6 | import { EventEmitter } from 'eventemitter3'; 7 | import Message from '../core/Message.js'; 8 | import ActionClient from './ActionClient.js'; 9 | 10 | /** 11 | * An actionlib goal that is associated with an action server. 12 | * 13 | * Emits the following events: 14 | * * 'timeout' - If a timeout occurred while sending a goal. 15 | */ 16 | export default class Goal extends EventEmitter { 17 | isFinished = false; 18 | status = undefined; 19 | result = undefined; 20 | feedback = undefined; 21 | // Create a random ID 22 | goalID = 'goal_' + Math.random() + '_' + new Date().getTime(); 23 | /** 24 | * @param {Object} options 25 | * @param {ActionClient} options.actionClient - The ROSLIB.ActionClient to use with this goal. 26 | * @param {Object} options.goalMessage - The JSON object containing the goal for the action server. 27 | */ 28 | constructor(options) { 29 | super(); 30 | this.actionClient = options.actionClient; 31 | 32 | // Fill in the goal message 33 | this.goalMessage = { 34 | goal_id: { 35 | stamp: { 36 | secs: 0, 37 | nsecs: 0 38 | }, 39 | id: this.goalID 40 | }, 41 | goal: options.goalMessage 42 | }; 43 | 44 | this.on('status', (status) => { 45 | this.status = status; 46 | }); 47 | 48 | this.on('result', (result) => { 49 | this.isFinished = true; 50 | this.result = result; 51 | }); 52 | 53 | this.on('feedback', (feedback) => { 54 | this.feedback = feedback; 55 | }); 56 | 57 | // Add the goal 58 | this.actionClient.goals[this.goalID] = this; 59 | } 60 | /** 61 | * Send the goal to the action server. 62 | * 63 | * @param {number} [timeout] - A timeout length for the goal's result. 64 | */ 65 | send(timeout) { 66 | this.actionClient.goalTopic.publish(this.goalMessage); 67 | if (timeout) { 68 | setTimeout(() => { 69 | if (!this.isFinished) { 70 | this.emit('timeout'); 71 | } 72 | }, timeout); 73 | } 74 | } 75 | /** 76 | * Cancel the current goal. 77 | */ 78 | cancel() { 79 | var cancelMessage = { 80 | id: this.goalID 81 | }; 82 | this.actionClient.cancelTopic.publish(cancelMessage); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/actionlib/SimpleActionServer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Laura Lindzey - lindzey@gmail.com 4 | */ 5 | 6 | import Topic from '../core/Topic.js'; 7 | import Ros from '../core/Ros.js'; 8 | import { EventEmitter } from 'eventemitter3'; 9 | 10 | /** 11 | * An actionlib action server client. 12 | * 13 | * Emits the following events: 14 | * * 'goal' - Goal sent by action client. 15 | * * 'cancel' - Action client has canceled the request. 16 | */ 17 | export default class SimpleActionServer extends EventEmitter { 18 | // needed for handling preemption prompted by a new goal being received 19 | /** @type {{goal_id: {id: any, stamp: any}, goal: any} | null} */ 20 | currentGoal = null; // currently tracked goal 21 | /** @type {{goal_id: {id: any, stamp: any}, goal: any} | null} */ 22 | nextGoal = null; // the one this'll be preempting 23 | /** 24 | * @param {Object} options 25 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 26 | * @param {string} options.serverName - The action server name, like '/fibonacci'. 27 | * @param {string} options.actionName - The action message name, like 'actionlib_tutorials/FibonacciAction'. 28 | */ 29 | constructor(options) { 30 | super(); 31 | this.ros = options.ros; 32 | this.serverName = options.serverName; 33 | this.actionName = options.actionName; 34 | 35 | // create and advertise publishers 36 | this.feedbackPublisher = new Topic({ 37 | ros: this.ros, 38 | name: this.serverName + '/feedback', 39 | messageType: this.actionName + 'Feedback' 40 | }); 41 | this.feedbackPublisher.advertise(); 42 | 43 | var statusPublisher = new Topic({ 44 | ros: this.ros, 45 | name: this.serverName + '/status', 46 | messageType: 'actionlib_msgs/GoalStatusArray' 47 | }); 48 | statusPublisher.advertise(); 49 | 50 | this.resultPublisher = new Topic({ 51 | ros: this.ros, 52 | name: this.serverName + '/result', 53 | messageType: this.actionName + 'Result' 54 | }); 55 | this.resultPublisher.advertise(); 56 | 57 | // create and subscribe to listeners 58 | var goalListener = new Topic({ 59 | ros: this.ros, 60 | name: this.serverName + '/goal', 61 | messageType: this.actionName + 'Goal' 62 | }); 63 | 64 | var cancelListener = new Topic({ 65 | ros: this.ros, 66 | name: this.serverName + '/cancel', 67 | messageType: 'actionlib_msgs/GoalID' 68 | }); 69 | 70 | // Track the goals and their status in order to publish status... 71 | this.statusMessage = { 72 | header: { 73 | stamp: { secs: 0, nsecs: 100 }, 74 | frame_id: '' 75 | }, 76 | /** @type {{goal_id: any, status: number}[]} */ 77 | status_list: [] 78 | }; 79 | 80 | goalListener.subscribe((goalMessage) => { 81 | if (this.currentGoal) { 82 | this.nextGoal = goalMessage; 83 | // needs to happen AFTER rest is set up 84 | this.emit('cancel'); 85 | } else { 86 | this.statusMessage.status_list = [{ goal_id: goalMessage.goal_id, status: 1 }]; 87 | this.currentGoal = goalMessage; 88 | this.emit('goal', goalMessage.goal); 89 | } 90 | }); 91 | 92 | // helper function to determine ordering of timestamps 93 | // returns t1 < t2 94 | var isEarlier = function (t1, t2) { 95 | if (t1.secs > t2.secs) { 96 | return false; 97 | } else if (t1.secs < t2.secs) { 98 | return true; 99 | } else if (t1.nsecs < t2.nsecs) { 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | }; 105 | 106 | // TODO: this may be more complicated than necessary, since I'm 107 | // not sure if the callbacks can ever wind up with a scenario 108 | // where we've been preempted by a next goal, it hasn't finished 109 | // processing, and then we get a cancel message 110 | cancelListener.subscribe((cancelMessage) => { 111 | // cancel ALL goals if both empty 112 | if ( 113 | cancelMessage.stamp.secs === 0 && 114 | cancelMessage.stamp.secs === 0 && 115 | cancelMessage.id === '' 116 | ) { 117 | this.nextGoal = null; 118 | if (this.currentGoal) { 119 | this.emit('cancel'); 120 | } 121 | } else { 122 | // treat id and stamp independently 123 | if ( 124 | this.currentGoal && 125 | cancelMessage.id === this.currentGoal.goal_id.id 126 | ) { 127 | this.emit('cancel'); 128 | } else if ( 129 | this.nextGoal && 130 | cancelMessage.id === this.nextGoal.goal_id.id 131 | ) { 132 | this.nextGoal = null; 133 | } 134 | 135 | if ( 136 | this.nextGoal && 137 | isEarlier(this.nextGoal.goal_id.stamp, cancelMessage.stamp) 138 | ) { 139 | this.nextGoal = null; 140 | } 141 | if ( 142 | this.currentGoal && 143 | isEarlier(this.currentGoal.goal_id.stamp, cancelMessage.stamp) 144 | ) { 145 | this.emit('cancel'); 146 | } 147 | } 148 | }); 149 | 150 | // publish status at pseudo-fixed rate; required for clients to know they've connected 151 | setInterval(() => { 152 | var currentTime = new Date(); 153 | var secs = Math.floor(currentTime.getTime() / 1000); 154 | var nsecs = Math.round( 155 | 1000000000 * (currentTime.getTime() / 1000 - secs) 156 | ); 157 | this.statusMessage.header.stamp.secs = secs; 158 | this.statusMessage.header.stamp.nsecs = nsecs; 159 | statusPublisher.publish(this.statusMessage); 160 | }, 500); // publish every 500ms 161 | } 162 | /** 163 | * Set action state to succeeded and return to client. 164 | * 165 | * @param {Object} result - The result to return to the client. 166 | */ 167 | setSucceeded(result) { 168 | if (this.currentGoal !== null) { 169 | var resultMessage = { 170 | status: { goal_id: this.currentGoal.goal_id, status: 3 }, 171 | result: result 172 | }; 173 | this.resultPublisher.publish(resultMessage); 174 | 175 | this.statusMessage.status_list = []; 176 | if (this.nextGoal) { 177 | this.currentGoal = this.nextGoal; 178 | this.nextGoal = null; 179 | this.emit('goal', this.currentGoal.goal); 180 | } else { 181 | this.currentGoal = null; 182 | } 183 | } 184 | } 185 | /** 186 | * Set action state to aborted and return to client. 187 | * 188 | * @param {Object} result - The result to return to the client. 189 | */ 190 | setAborted(result) { 191 | if (this.currentGoal !== null) { 192 | var resultMessage = { 193 | status: { goal_id: this.currentGoal.goal_id, status: 4 }, 194 | result: result 195 | }; 196 | this.resultPublisher.publish(resultMessage); 197 | 198 | this.statusMessage.status_list = []; 199 | if (this.nextGoal) { 200 | this.currentGoal = this.nextGoal; 201 | this.nextGoal = null; 202 | this.emit('goal', this.currentGoal.goal); 203 | } else { 204 | this.currentGoal = null; 205 | } 206 | } 207 | } 208 | /** 209 | * Send a feedback message. 210 | * 211 | * @param {Object} feedback - The feedback to send to the client. 212 | */ 213 | sendFeedback(feedback) { 214 | if (this.currentGoal !== null) { 215 | var feedbackMessage = { 216 | status: { goal_id: this.currentGoal.goal_id, status: 1 }, 217 | feedback: feedback 218 | }; 219 | this.feedbackPublisher.publish(feedbackMessage); 220 | } 221 | } 222 | /** 223 | * Handle case where client requests preemption. 224 | */ 225 | setPreempted() { 226 | if (this.currentGoal !== null) { 227 | this.statusMessage.status_list = []; 228 | var resultMessage = { 229 | status: { goal_id: this.currentGoal.goal_id, status: 2 } 230 | }; 231 | this.resultPublisher.publish(resultMessage); 232 | 233 | if (this.nextGoal) { 234 | this.currentGoal = this.nextGoal; 235 | this.nextGoal = null; 236 | this.emit('goal', this.currentGoal.goal); 237 | } else { 238 | this.currentGoal = null; 239 | } 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/actionlib/index.js: -------------------------------------------------------------------------------- 1 | export { default as ActionClient } from './ActionClient.js'; 2 | export { default as ActionListener } from './ActionListener.js'; 3 | export { default as Goal } from './Goal.js'; 4 | export { default as SimpleActionServer } from './SimpleActionServer.js'; 5 | -------------------------------------------------------------------------------- /src/core/Action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Sebastian Castro - sebastian.castro@picknik.ai 4 | */ 5 | 6 | import { EventEmitter } from 'eventemitter3'; 7 | import Ros from '../core/Ros.js'; 8 | import { GoalStatus } from '../core/GoalStatus'; 9 | 10 | /** 11 | * A ROS 2 action client. 12 | * @template TGoal, TFeedback, TResult 13 | */ 14 | export default class Action extends EventEmitter { 15 | isAdvertised = false; 16 | /** 17 | * @callback advertiseActionCallback 18 | * @param {TGoal} goal - The action goal. 19 | * @param {string} id - The ID of the action goal to execute. 20 | */ 21 | /** 22 | * @private 23 | * @type {advertiseActionCallback | null} 24 | */ 25 | _actionCallback = null; 26 | /** 27 | * @callback advertiseCancelCallback 28 | * @param {string} id - The ID of the action goal to cancel. 29 | */ 30 | /** 31 | * @private 32 | * @type {advertiseCancelCallback | null} 33 | */ 34 | _cancelCallback = null; 35 | /** 36 | * @param {Object} options 37 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 38 | * @param {string} options.name - The action name, like '/fibonacci'. 39 | * @param {string} options.actionType - The action type, like 'action_tutorials_interfaces/Fibonacci'. 40 | */ 41 | constructor(options) { 42 | super(); 43 | this.ros = options.ros; 44 | this.name = options.name; 45 | this.actionType = options.actionType; 46 | } 47 | 48 | /** 49 | * @callback sendGoalResultCallback 50 | * @param {TResult} result - The result from the action. 51 | */ 52 | /** 53 | * @callback sendGoalFeedbackCallback 54 | * @param {TFeedback} feedback - The feedback from the action. 55 | */ 56 | /** 57 | * @callback sendGoalFailedCallback 58 | * @param {string} error - The error message reported by ROS. 59 | */ 60 | /** 61 | * Sends an action goal. Returns the feedback in the feedback callback while the action is running 62 | * and the result in the result callback when the action is completed. 63 | * Does nothing if this action is currently advertised. 64 | * 65 | * @param {TGoal} goal - The action goal to send. 66 | * @param {sendGoalResultCallback} resultCallback - The callback function when the action is completed. 67 | * @param {sendGoalFeedbackCallback} [feedbackCallback] - The callback function when the action pulishes feedback. 68 | * @param {sendGoalFailedCallback} [failedCallback] - The callback function when the action failed. 69 | */ 70 | sendGoal(goal, resultCallback, feedbackCallback, failedCallback) { 71 | if (this.isAdvertised) { 72 | return; 73 | } 74 | 75 | var actionGoalId = 76 | 'send_action_goal:' + this.name + ':' + ++this.ros.idCounter; 77 | 78 | if (resultCallback || failedCallback) { 79 | this.ros.on(actionGoalId, function (message) { 80 | if (message.result !== undefined && message.result === false) { 81 | if (typeof failedCallback === 'function') { 82 | failedCallback(message.values); 83 | } 84 | } else if ( 85 | message.op === 'action_feedback' && 86 | typeof feedbackCallback === 'function' 87 | ) { 88 | feedbackCallback(message.values); 89 | } else if ( 90 | message.op === 'action_result' && 91 | typeof resultCallback === 'function' 92 | ) { 93 | resultCallback(message.values); 94 | } 95 | }); 96 | } 97 | 98 | var call = { 99 | op: 'send_action_goal', 100 | id: actionGoalId, 101 | action: this.name, 102 | action_type: this.actionType, 103 | args: goal, 104 | feedback: true, 105 | }; 106 | this.ros.callOnConnection(call); 107 | 108 | return actionGoalId; 109 | } 110 | 111 | /** 112 | * Cancels an action goal. 113 | * 114 | * @param {string} id - The ID of the action goal to cancel. 115 | */ 116 | cancelGoal(id) { 117 | var call = { 118 | op: 'cancel_action_goal', 119 | id: id, 120 | action: this.name 121 | }; 122 | this.ros.callOnConnection(call); 123 | } 124 | 125 | /** 126 | * Advertise the action. This turns the Action object from a client 127 | * into a server. The callback will be called with every goal sent to this action. 128 | * 129 | * @param {advertiseActionCallback} actionCallback - This works similarly to the callback for a C++ action. 130 | * @param {advertiseCancelCallback} cancelCallback - A callback function to execute when the action is canceled. 131 | */ 132 | advertise(actionCallback, cancelCallback) { 133 | if (this.isAdvertised || typeof actionCallback !== 'function') { 134 | return; 135 | } 136 | 137 | this._actionCallback = actionCallback; 138 | this._cancelCallback = cancelCallback; 139 | this.ros.on(this.name, this._executeAction.bind(this)); 140 | this.ros.callOnConnection({ 141 | op: 'advertise_action', 142 | type: this.actionType, 143 | action: this.name 144 | }); 145 | this.isAdvertised = true; 146 | } 147 | 148 | /** 149 | * Unadvertise a previously advertised action. 150 | */ 151 | unadvertise() { 152 | if (!this.isAdvertised) { 153 | return; 154 | } 155 | this.ros.callOnConnection({ 156 | op: 'unadvertise_action', 157 | action: this.name 158 | }); 159 | this.isAdvertised = false; 160 | } 161 | 162 | /** 163 | * Helper function that executes an action by calling the provided 164 | * action callback with the auto-generated ID as a user-accessible input. 165 | * Should not be called manually. 166 | * 167 | * @param {Object} rosbridgeRequest - The rosbridge request containing the action goal to send and its ID. 168 | * @param {string} rosbridgeRequest.id - The ID of the action goal. 169 | * @param {TGoal} rosbridgeRequest.args - The arguments of the action goal. 170 | */ 171 | _executeAction(rosbridgeRequest) { 172 | var id = rosbridgeRequest.id; 173 | 174 | // If a cancellation callback exists, call it when a cancellation event is emitted. 175 | if (typeof id === 'string') { 176 | this.ros.on(id, (message) => { 177 | if ( 178 | message.op === 'cancel_action_goal' && 179 | typeof this._cancelCallback === 'function' 180 | ) { 181 | this._cancelCallback(id); 182 | } 183 | }); 184 | } 185 | 186 | // Call the action goal execution function provided. 187 | if (typeof this._actionCallback === 'function') { 188 | this._actionCallback(rosbridgeRequest.args, id); 189 | } 190 | } 191 | 192 | /** 193 | * Helper function to send action feedback inside an action handler. 194 | * 195 | * @param {string} id - The action goal ID. 196 | * @param {TFeedback} feedback - The feedback to send. 197 | */ 198 | sendFeedback(id, feedback) { 199 | var call = { 200 | op: 'action_feedback', 201 | id: id, 202 | action: this.name, 203 | values: feedback 204 | }; 205 | this.ros.callOnConnection(call); 206 | } 207 | 208 | /** 209 | * Helper function to set an action as succeeded. 210 | * 211 | * @param {string} id - The action goal ID. 212 | * @param {TResult} result - The result to set. 213 | */ 214 | setSucceeded(id, result) { 215 | var call = { 216 | op: 'action_result', 217 | id: id, 218 | action: this.name, 219 | values: result, 220 | status: GoalStatus.STATUS_SUCCEEDED, 221 | result: true 222 | }; 223 | this.ros.callOnConnection(call); 224 | } 225 | 226 | /** 227 | * Helper function to set an action as canceled. 228 | * 229 | * @param {string} id - The action goal ID. 230 | * @param {TResult} result - The result to set. 231 | */ 232 | setCanceled(id, result) { 233 | var call = { 234 | op: 'action_result', 235 | id: id, 236 | action: this.name, 237 | values: result, 238 | status: GoalStatus.STATUS_CANCELED, 239 | result: true 240 | }; 241 | this.ros.callOnConnection(call); 242 | } 243 | 244 | /** 245 | * Helper function to set an action as failed. 246 | * 247 | * @param {string} id - The action goal ID. 248 | */ 249 | setFailed(id) { 250 | var call = { 251 | op: 'action_result', 252 | id: id, 253 | action: this.name, 254 | status: GoalStatus.STATUS_ABORTED, 255 | result: false 256 | }; 257 | this.ros.callOnConnection(call); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/core/GoalStatus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An enumeration for goal statuses. 3 | * This is directly based on the action_msgs/GoalStatus ROS message: 4 | * https://docs.ros2.org/latest/api/action_msgs/msg/GoalStatus.html 5 | */ 6 | export enum GoalStatus { 7 | STATUS_UNKNOWN = 0, 8 | STATUS_ACCEPTED = 1, 9 | STATUS_EXECUTING = 2, 10 | STATUS_CANCELING = 3, 11 | STATUS_SUCCEEDED = 4, 12 | STATUS_CANCELED = 5, 13 | STATUS_ABORTED = 6 14 | } 15 | -------------------------------------------------------------------------------- /src/core/Message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Brandon Alexander - baalexander@gmail.com 4 | */ 5 | 6 | /** 7 | * Message objects are used for publishing and subscribing to and from topics. 8 | * 9 | * @constructor 10 | * @template T 11 | */ 12 | export default class Message { 13 | /** 14 | * @param {T} [values={}] - An object matching the fields defined in the .msg definition file. 15 | */ 16 | constructor(values) { 17 | Object.assign(this, values || {}); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/Param.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Brandon Alexander - baalexander@gmail.com 4 | */ 5 | 6 | import Service from './Service.js'; 7 | import Ros from '../core/Ros.js'; 8 | 9 | /** 10 | * A ROS parameter. 11 | */ 12 | export default class Param { 13 | /** 14 | * @param {Object} options 15 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 16 | * @param {string} options.name - The param name, like max_vel_x. 17 | */ 18 | constructor(options) { 19 | this.ros = options.ros; 20 | this.name = options.name; 21 | } 22 | /** 23 | * @callback getCallback 24 | * @param {Object} value - The value of the param from ROS. 25 | */ 26 | /** 27 | * @callback getFailedCallback 28 | * @param {string} error - The error message reported by ROS. 29 | */ 30 | /** 31 | * Fetch the value of the param. 32 | * 33 | * @param {getCallback} callback - The callback function. 34 | * @param {getFailedCallback} [failedCallback] - The callback function when the service call failed. 35 | */ 36 | get(callback, failedCallback) { 37 | var paramClient = new Service({ 38 | ros: this.ros, 39 | name: 'rosapi/get_param', 40 | serviceType: 'rosapi/GetParam' 41 | }); 42 | 43 | var request = {name: this.name}; 44 | 45 | paramClient.callService( 46 | request, 47 | function (result) { 48 | var value = JSON.parse(result.value); 49 | callback(value); 50 | }, 51 | failedCallback 52 | ); 53 | } 54 | /** 55 | * @callback setParamCallback 56 | * @param {Object} response - The response from the service request. 57 | */ 58 | /** 59 | * @callback setParamFailedCallback 60 | * @param {string} error - The error message reported by ROS. 61 | */ 62 | /** 63 | * Set the value of the param in ROS. 64 | * 65 | * @param {Object} value - The value to set param to. 66 | * @param {setParamCallback} [callback] - The callback function. 67 | * @param {setParamFailedCallback} [failedCallback] - The callback function when the service call failed. 68 | */ 69 | set(value, callback, failedCallback) { 70 | var paramClient = new Service({ 71 | ros: this.ros, 72 | name: 'rosapi/set_param', 73 | serviceType: 'rosapi/SetParam' 74 | }); 75 | 76 | var request = { 77 | name: this.name, 78 | value: JSON.stringify(value) 79 | }; 80 | 81 | paramClient.callService(request, callback, failedCallback); 82 | } 83 | /** 84 | * Delete this parameter on the ROS server. 85 | * 86 | * @param {setParamCallback} callback - The callback function. 87 | * @param {setParamFailedCallback} [failedCallback] - The callback function when the service call failed. 88 | */ 89 | delete(callback, failedCallback) { 90 | var paramClient = new Service({ 91 | ros: this.ros, 92 | name: 'rosapi/delete_param', 93 | serviceType: 'rosapi/DeleteParam' 94 | }); 95 | 96 | var request = { 97 | name: this.name 98 | }; 99 | 100 | paramClient.callService(request, callback, failedCallback); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/core/Service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Brandon Alexander - baalexander@gmail.com 4 | */ 5 | 6 | import Ros from './Ros.js'; 7 | import { EventEmitter } from 'eventemitter3'; 8 | 9 | /** 10 | * A ROS service client. 11 | * @template TRequest, TResponse 12 | */ 13 | export default class Service extends EventEmitter { 14 | /** 15 | * Stores a reference to the most recent service callback advertised so it can be removed from the EventEmitter during un-advertisement 16 | * @private 17 | * @type {((rosbridgeRequest) => any) | null} 18 | */ 19 | _serviceCallback = null; 20 | isAdvertised = false; 21 | /** 22 | * @param {Object} options 23 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 24 | * @param {string} options.name - The service name, like '/add_two_ints'. 25 | * @param {string} options.serviceType - The service type, like 'rospy_tutorials/AddTwoInts'. 26 | */ 27 | constructor(options) { 28 | super(); 29 | this.ros = options.ros; 30 | this.name = options.name; 31 | this.serviceType = options.serviceType; 32 | } 33 | /** 34 | * @callback callServiceCallback 35 | * @param {TResponse} response - The response from the service request. 36 | */ 37 | /** 38 | * @callback callServiceFailedCallback 39 | * @param {string} error - The error message reported by ROS. 40 | */ 41 | /** 42 | * Call the service. Returns the service response in the 43 | * callback. Does nothing if this service is currently advertised. 44 | * 45 | * @param {TRequest} request - The service request to send. 46 | * @param {callServiceCallback} [callback] - Function with the following params: 47 | * @param {callServiceFailedCallback} [failedCallback] - The callback function when the service call failed with params: 48 | * @param {number} [timeout] - Optional timeout, in seconds, for the service call. A non-positive value means no timeout. 49 | * If not provided, the rosbridge server will use its default value. 50 | */ 51 | callService(request, callback, failedCallback, timeout) { 52 | if (this.isAdvertised) { 53 | return; 54 | } 55 | 56 | var serviceCallId = 57 | 'call_service:' + this.name + ':' + (++this.ros.idCounter).toString(); 58 | 59 | if (callback || failedCallback) { 60 | this.ros.once(serviceCallId, function (message) { 61 | if (message.result !== undefined && message.result === false) { 62 | if (typeof failedCallback === 'function') { 63 | failedCallback(message.values); 64 | } 65 | } else if (typeof callback === 'function') { 66 | callback(message.values); 67 | } 68 | }); 69 | } 70 | 71 | var call = { 72 | op: 'call_service', 73 | id: serviceCallId, 74 | service: this.name, 75 | type: this.serviceType, 76 | args: request, 77 | timeout: timeout 78 | }; 79 | 80 | this.ros.callOnConnection(call); 81 | } 82 | /** 83 | * @callback advertiseCallback 84 | * @param {TRequest} request - The service request. 85 | * @param {Partial} response - An empty dictionary. Take care not to overwrite this. Instead, only modify the values within. 86 | * @returns {boolean} true if the service has finished successfully, i.e., without any fatal errors. 87 | */ 88 | /** 89 | * Advertise the service. This turns the Service object from a client 90 | * into a server. The callback will be called with every request 91 | * that's made on this service. 92 | * 93 | * @param {advertiseCallback} callback - This works similarly to the callback for a C++ service and should take the following params 94 | */ 95 | advertise(callback) { 96 | if (this.isAdvertised) { 97 | throw new Error('Cannot advertise the same Service twice!'); 98 | } 99 | 100 | // Store the new callback for removal during un-advertisement 101 | this._serviceCallback = (rosbridgeRequest) => { 102 | var response = {}; 103 | var success = callback(rosbridgeRequest.args, response); 104 | 105 | var call = { 106 | op: 'service_response', 107 | service: this.name, 108 | values: response, 109 | result: success 110 | }; 111 | 112 | if (rosbridgeRequest.id) { 113 | call.id = rosbridgeRequest.id; 114 | } 115 | 116 | this.ros.callOnConnection(call); 117 | }; 118 | 119 | this.ros.on(this.name, this._serviceCallback); 120 | this.ros.callOnConnection({ 121 | op: 'advertise_service', 122 | type: this.serviceType, 123 | service: this.name 124 | }); 125 | this.isAdvertised = true; 126 | } 127 | 128 | unadvertise() { 129 | if (!this.isAdvertised) { 130 | throw new Error(`Tried to un-advertise service ${this.name}, but it was not advertised!`); 131 | } 132 | this.ros.callOnConnection({ 133 | op: 'unadvertise_service', 134 | service: this.name 135 | }); 136 | // Remove the registered callback 137 | if (this._serviceCallback) { 138 | this.ros.off(this.name, this._serviceCallback); 139 | } 140 | this.isAdvertised = false; 141 | } 142 | 143 | /** 144 | * An alternate form of Service advertisement that supports a modern Promise-based interface for use with async/await. 145 | * @param {(request: TRequest) => Promise} callback An asynchronous callback processing the request and returning a response. 146 | */ 147 | advertiseAsync(callback) { 148 | if (this.isAdvertised) { 149 | throw new Error('Cannot advertise the same Service twice!'); 150 | } 151 | this._serviceCallback = async (rosbridgeRequest) => { 152 | /** @type {{op: string, service: string, values?: TResponse, result: boolean, id?: string}} */ 153 | let rosbridgeResponse = { 154 | op: 'service_response', 155 | service: this.name, 156 | result: false 157 | } 158 | try { 159 | rosbridgeResponse.values = await callback(rosbridgeRequest.args); 160 | rosbridgeResponse.result = true; 161 | } finally { 162 | if (rosbridgeRequest.id) { 163 | rosbridgeResponse.id = rosbridgeRequest.id; 164 | } 165 | this.ros.callOnConnection(rosbridgeResponse); 166 | } 167 | } 168 | this.ros.on(this.name, this._serviceCallback); 169 | this.ros.callOnConnection({ 170 | op: 'advertise_service', 171 | type: this.serviceType, 172 | service: this.name 173 | }); 174 | this.isAdvertised = true; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/core/SocketAdapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket event handling utilities for handling events on either 3 | * WebSocket and TCP sockets 4 | * 5 | * Note to anyone reviewing this code: these functions are called 6 | * in the context of their parent object, unless bound 7 | * @fileOverview 8 | */ 9 | 10 | import CBOR from 'cbor-js'; 11 | import typedArrayTagger from '../util/cborTypedArrayTags.js'; 12 | var BSON = null; 13 | // @ts-expect-error -- Workarounds for not including BSON in bundle. need to revisit 14 | if (typeof bson !== 'undefined') { 15 | // @ts-expect-error -- Workarounds for not including BSON in bundle. need to revisit 16 | BSON = bson().BSON; 17 | } 18 | 19 | /** 20 | * Event listeners for a WebSocket or TCP socket to a JavaScript 21 | * ROS Client. Sets up Messages for a given topic to trigger an 22 | * event on the ROS client. 23 | * 24 | * @namespace SocketAdapter 25 | * @private 26 | */ 27 | export default function SocketAdapter(client) { 28 | var decoder = null; 29 | if (client.transportOptions.decoder) { 30 | decoder = client.transportOptions.decoder; 31 | } 32 | 33 | function handleMessage(message) { 34 | if (message.op === 'publish') { 35 | client.emit(message.topic, message.msg); 36 | } else if (message.op === 'service_response') { 37 | client.emit(message.id, message); 38 | } else if (message.op === 'call_service') { 39 | client.emit(message.service, message); 40 | } else if (message.op === 'send_action_goal') { 41 | client.emit(message.action, message); 42 | } else if (message.op === 'cancel_action_goal') { 43 | client.emit(message.id, message); 44 | } else if (message.op === 'action_feedback') { 45 | client.emit(message.id, message); 46 | } else if (message.op === 'action_result') { 47 | client.emit(message.id, message); 48 | } else if (message.op === 'status') { 49 | if (message.id) { 50 | client.emit('status:' + message.id, message); 51 | } else { 52 | client.emit('status', message); 53 | } 54 | } 55 | } 56 | 57 | function handlePng(message, callback) { 58 | if (message.op === 'png') { 59 | // If in Node.js.. 60 | if (typeof window === 'undefined') { 61 | import('../util/decompressPng.js').then(({ default: decompressPng }) => decompressPng(message.data, callback)); 62 | } else { 63 | // if in browser.. 64 | import('../util/shim/decompressPng.js').then(({default: decompressPng}) => decompressPng(message.data, callback)); 65 | } 66 | } else { 67 | callback(message); 68 | } 69 | } 70 | 71 | function decodeBSON(data, callback) { 72 | if (!BSON) { 73 | throw 'Cannot process BSON encoded message without BSON header.'; 74 | } 75 | var reader = new FileReader(); 76 | reader.onload = function () { 77 | // @ts-expect-error -- this doesn't seem right, but don't want to break current type coercion assumption 78 | var uint8Array = new Uint8Array(this.result); 79 | var msg = BSON.deserialize(uint8Array); 80 | callback(msg); 81 | }; 82 | reader.readAsArrayBuffer(data); 83 | } 84 | 85 | return { 86 | /** 87 | * Emit a 'connection' event on WebSocket connection. 88 | * 89 | * @param {function} event - The argument to emit with the event. 90 | * @memberof SocketAdapter 91 | */ 92 | onopen: function onOpen(event) { 93 | client.isConnected = true; 94 | client.emit('connection', event); 95 | }, 96 | 97 | /** 98 | * Emit a 'close' event on WebSocket disconnection. 99 | * 100 | * @param {function} event - The argument to emit with the event. 101 | * @memberof SocketAdapter 102 | */ 103 | onclose: function onClose(event) { 104 | client.isConnected = false; 105 | client.emit('close', event); 106 | }, 107 | 108 | /** 109 | * Emit an 'error' event whenever there was an error. 110 | * 111 | * @param {function} event - The argument to emit with the event. 112 | * @memberof SocketAdapter 113 | */ 114 | onerror: function onError(event) { 115 | client.emit('error', event); 116 | }, 117 | 118 | /** 119 | * Parse message responses from rosbridge and send to the appropriate 120 | * topic, service, or param. 121 | * 122 | * @param {Object} data - The raw JSON message from rosbridge. 123 | * @memberof SocketAdapter 124 | */ 125 | onmessage: function onMessage(data) { 126 | if (decoder) { 127 | decoder(data.data, function (message) { 128 | handleMessage(message); 129 | }); 130 | } else if (typeof Blob !== 'undefined' && data.data instanceof Blob) { 131 | decodeBSON(data.data, function (message) { 132 | handlePng(message, handleMessage); 133 | }); 134 | } else if (data.data instanceof ArrayBuffer) { 135 | var decoded = CBOR.decode(data.data, typedArrayTagger); 136 | handleMessage(decoded); 137 | } else { 138 | var message = JSON.parse(typeof data === 'string' ? data : data.data); 139 | handlePng(message, handleMessage); 140 | } 141 | } 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /src/core/Topic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Brandon Alexander - baalexander@gmail.com 4 | */ 5 | 6 | import { EventEmitter } from 'eventemitter3'; 7 | import Ros from './Ros.js'; 8 | 9 | /** 10 | * Publish and/or subscribe to a topic in ROS. 11 | * 12 | * Emits the following events: 13 | * * 'warning' - If there are any warning during the Topic creation. 14 | * * 'message' - The message data from rosbridge. 15 | * @template T 16 | */ 17 | export default class Topic extends EventEmitter { 18 | /** @type {boolean | undefined} */ 19 | waitForReconnect = undefined; 20 | /** @type {(() => void) | undefined} */ 21 | reconnectFunc = undefined; 22 | isAdvertised = false; 23 | /** 24 | * @param {Object} options 25 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 26 | * @param {string} options.name - The topic name, like '/cmd_vel'. 27 | * @param {string} options.messageType - The message type, like 'std_msgs/String'. 28 | * @param {string} [options.compression=none] - The type of compression to use, like 'png', 'cbor', or 'cbor-raw'. 29 | * @param {number} [options.throttle_rate=0] - The rate (in ms in between messages) at which to throttle the topics. 30 | * @param {number} [options.queue_size=100] - The queue created at bridge side for re-publishing webtopics. 31 | * @param {boolean} [options.latch=false] - Latch the topic when publishing. 32 | * @param {number} [options.queue_length=0] - The queue length at bridge side used when subscribing. 33 | * @param {boolean} [options.reconnect_on_close=true] - The flag to enable resubscription and readvertisement on close event. 34 | */ 35 | constructor(options) { 36 | super(); 37 | this.ros = options.ros; 38 | this.name = options.name; 39 | this.messageType = options.messageType; 40 | this.compression = options.compression || 'none'; 41 | this.throttle_rate = options.throttle_rate || 0; 42 | this.latch = options.latch || false; 43 | this.queue_size = options.queue_size || 100; 44 | this.queue_length = options.queue_length || 0; 45 | this.reconnect_on_close = 46 | options.reconnect_on_close !== undefined 47 | ? options.reconnect_on_close 48 | : true; 49 | 50 | // Check for valid compression types 51 | if ( 52 | this.compression && 53 | this.compression !== 'png' && 54 | this.compression !== 'cbor' && 55 | this.compression !== 'cbor-raw' && 56 | this.compression !== 'none' 57 | ) { 58 | this.emit( 59 | 'warning', 60 | this.compression + 61 | ' compression is not supported. No compression will be used.' 62 | ); 63 | this.compression = 'none'; 64 | } 65 | 66 | // Check if throttle rate is negative 67 | if (this.throttle_rate < 0) { 68 | this.emit('warning', this.throttle_rate + ' is not allowed. Set to 0'); 69 | this.throttle_rate = 0; 70 | } 71 | 72 | if (this.reconnect_on_close) { 73 | this.callForSubscribeAndAdvertise = (message) => { 74 | this.ros.callOnConnection(message); 75 | 76 | this.waitForReconnect = false; 77 | this.reconnectFunc = () => { 78 | if (!this.waitForReconnect) { 79 | this.waitForReconnect = true; 80 | this.ros.callOnConnection(message); 81 | this.ros.once('connection', () => { 82 | this.waitForReconnect = false; 83 | }); 84 | } 85 | }; 86 | this.ros.on('close', this.reconnectFunc); 87 | }; 88 | } else { 89 | this.callForSubscribeAndAdvertise = this.ros.callOnConnection; 90 | } 91 | } 92 | 93 | _messageCallback = (data) => { 94 | this.emit('message', data); 95 | }; 96 | /** 97 | * @callback subscribeCallback 98 | * @param {T} message - The published message. 99 | */ 100 | /** 101 | * Every time a message is published for the given topic, the callback 102 | * will be called with the message object. 103 | * 104 | * @param {subscribeCallback} callback - Function with the following params: 105 | */ 106 | subscribe(callback) { 107 | if (typeof callback === 'function') { 108 | this.on('message', callback); 109 | } 110 | 111 | if (this.subscribeId) { 112 | return; 113 | } 114 | this.ros.on(this.name, this._messageCallback); 115 | this.subscribeId = 116 | 'subscribe:' + this.name + ':' + (++this.ros.idCounter).toString(); 117 | 118 | this.callForSubscribeAndAdvertise({ 119 | op: 'subscribe', 120 | id: this.subscribeId, 121 | type: this.messageType, 122 | topic: this.name, 123 | compression: this.compression, 124 | throttle_rate: this.throttle_rate, 125 | queue_length: this.queue_length 126 | }); 127 | } 128 | /** 129 | * Unregister as a subscriber for the topic. Unsubscribing will stop 130 | * and remove all subscribe callbacks. To remove a callback, you must 131 | * explicitly pass the callback function in. 132 | * 133 | * @param {import('eventemitter3').EventEmitter.ListenerFn} [callback] - The callback to unregister, if 134 | * provided and other listeners are registered the topic won't 135 | * unsubscribe, just stop emitting to the passed listener. 136 | */ 137 | unsubscribe(callback) { 138 | if (callback) { 139 | this.off('message', callback); 140 | // If there is any other callbacks still subscribed don't unsubscribe 141 | if (this.listeners('message').length) { 142 | return; 143 | } 144 | } 145 | if (!this.subscribeId) { 146 | return; 147 | } 148 | // Note: Don't call this.removeAllListeners, allow client to handle that themselves 149 | this.ros.off(this.name, this._messageCallback); 150 | if (this.reconnect_on_close) { 151 | this.ros.off('close', this.reconnectFunc); 152 | } 153 | this.emit('unsubscribe'); 154 | this.ros.callOnConnection({ 155 | op: 'unsubscribe', 156 | id: this.subscribeId, 157 | topic: this.name 158 | }); 159 | this.subscribeId = null; 160 | } 161 | /** 162 | * Register as a publisher for the topic. 163 | */ 164 | advertise() { 165 | if (this.isAdvertised) { 166 | return; 167 | } 168 | this.advertiseId = 169 | 'advertise:' + this.name + ':' + (++this.ros.idCounter).toString(); 170 | this.callForSubscribeAndAdvertise({ 171 | op: 'advertise', 172 | id: this.advertiseId, 173 | type: this.messageType, 174 | topic: this.name, 175 | latch: this.latch, 176 | queue_size: this.queue_size 177 | }); 178 | this.isAdvertised = true; 179 | 180 | if (!this.reconnect_on_close) { 181 | this.ros.on('close', () => { 182 | this.isAdvertised = false; 183 | }); 184 | } 185 | } 186 | /** 187 | * Unregister as a publisher for the topic. 188 | */ 189 | unadvertise() { 190 | if (!this.isAdvertised) { 191 | return; 192 | } 193 | if (this.reconnect_on_close) { 194 | this.ros.off('close', this.reconnectFunc); 195 | } 196 | this.emit('unadvertise'); 197 | this.ros.callOnConnection({ 198 | op: 'unadvertise', 199 | id: this.advertiseId, 200 | topic: this.name 201 | }); 202 | this.isAdvertised = false; 203 | } 204 | /** 205 | * Publish the message. 206 | * 207 | * @param {T} message - The message to publish. 208 | */ 209 | publish(message) { 210 | if (!this.isAdvertised) { 211 | this.advertise(); 212 | } 213 | 214 | this.ros.idCounter++; 215 | var call = { 216 | op: 'publish', 217 | id: 'publish:' + this.name + ':' + this.ros.idCounter, 218 | topic: this.name, 219 | msg: message, 220 | latch: this.latch 221 | }; 222 | this.ros.callOnConnection(call); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | export {default as Ros} from './Ros.js'; 2 | export {default as Topic} from './Topic.js'; 3 | export {default as Param} from './Param.js'; 4 | export {default as Service} from './Service.js'; 5 | export {default as Action} from './Action.js'; 6 | -------------------------------------------------------------------------------- /src/math/Pose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author David Gossow - dgossow@willowgarage.com 4 | */ 5 | 6 | import Vector3 from './Vector3.js'; 7 | import Quaternion from './Quaternion.js'; 8 | import Transform from './Transform.js'; 9 | 10 | /** 11 | * A Pose in 3D space. Values are copied into this object. 12 | */ 13 | export default class Pose { 14 | /** 15 | * @param {Object} [options] 16 | * @param {Vector3} [options.position] - The ROSLIB.Vector3 describing the position. 17 | * @param {Quaternion} [options.orientation] - The ROSLIB.Quaternion describing the orientation. 18 | */ 19 | constructor(options) { 20 | options = options || {}; 21 | // copy the values into this object if they exist 22 | options = options || {}; 23 | this.position = new Vector3(options.position); 24 | this.orientation = new Quaternion(options.orientation); 25 | } 26 | /** 27 | * Apply a transform against this pose. 28 | * 29 | * @param {Transform} tf - The transform to be applied. 30 | */ 31 | applyTransform(tf) { 32 | this.position.multiplyQuaternion(tf.rotation); 33 | this.position.add(tf.translation); 34 | var tmp = tf.rotation.clone(); 35 | tmp.multiply(this.orientation); 36 | this.orientation = tmp; 37 | } 38 | /** 39 | * Clone a copy of this pose. 40 | * 41 | * @returns {Pose} The cloned pose. 42 | */ 43 | clone() { 44 | return new Pose(this); 45 | } 46 | /** 47 | * Multiply this pose with another pose without altering this pose. 48 | * 49 | * @returns {Pose} The result of the multiplication. 50 | */ 51 | multiply(pose) { 52 | var p = pose.clone(); 53 | p.applyTransform({ 54 | rotation: this.orientation, 55 | translation: this.position 56 | }); 57 | return p; 58 | } 59 | /** 60 | * Compute the inverse of this pose. 61 | * 62 | * @returns {Pose} The inverse of the pose. 63 | */ 64 | getInverse() { 65 | var inverse = this.clone(); 66 | inverse.orientation.invert(); 67 | inverse.position.multiplyQuaternion(inverse.orientation); 68 | inverse.position.x *= -1; 69 | inverse.position.y *= -1; 70 | inverse.position.z *= -1; 71 | return inverse; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/math/Quaternion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author David Gossow - dgossow@willowgarage.com 4 | */ 5 | 6 | /** 7 | * A Quaternion. 8 | */ 9 | export default class Quaternion { 10 | /** 11 | * @param {Object} [options] 12 | * @param {number|null} [options.x=0] - The x value. 13 | * @param {number|null} [options.y=0] - The y value. 14 | * @param {number|null} [options.z=0] - The z value. 15 | * @param {number|null} [options.w=1] - The w value. 16 | */ 17 | constructor(options) { 18 | options = options || {}; 19 | this.x = options.x || 0; 20 | this.y = options.y || 0; 21 | this.z = options.z || 0; 22 | this.w = typeof options.w === 'number' ? options.w : 1; 23 | } 24 | /** 25 | * Perform a conjugation on this quaternion. 26 | */ 27 | conjugate() { 28 | this.x *= -1; 29 | this.y *= -1; 30 | this.z *= -1; 31 | } 32 | /** 33 | * Return the norm of this quaternion. 34 | */ 35 | norm() { 36 | return Math.sqrt( 37 | this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w 38 | ); 39 | } 40 | /** 41 | * Perform a normalization on this quaternion. 42 | */ 43 | normalize() { 44 | var l = Math.sqrt( 45 | this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w 46 | ); 47 | if (l === 0) { 48 | this.x = 0; 49 | this.y = 0; 50 | this.z = 0; 51 | this.w = 1; 52 | } else { 53 | l = 1 / l; 54 | this.x = this.x * l; 55 | this.y = this.y * l; 56 | this.z = this.z * l; 57 | this.w = this.w * l; 58 | } 59 | } 60 | /** 61 | * Convert this quaternion into its inverse. 62 | */ 63 | invert() { 64 | this.conjugate(); 65 | this.normalize(); 66 | } 67 | /** 68 | * Set the values of this quaternion to the product of itself and the given quaternion. 69 | * 70 | * @param {Quaternion} q - The quaternion to multiply with. 71 | */ 72 | multiply(q) { 73 | var newX = this.x * q.w + this.y * q.z - this.z * q.y + this.w * q.x; 74 | var newY = -this.x * q.z + this.y * q.w + this.z * q.x + this.w * q.y; 75 | var newZ = this.x * q.y - this.y * q.x + this.z * q.w + this.w * q.z; 76 | var newW = -this.x * q.x - this.y * q.y - this.z * q.z + this.w * q.w; 77 | this.x = newX; 78 | this.y = newY; 79 | this.z = newZ; 80 | this.w = newW; 81 | } 82 | /** 83 | * Clone a copy of this quaternion. 84 | * 85 | * @returns {Quaternion} The cloned quaternion. 86 | */ 87 | clone() { 88 | return new Quaternion(this); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/math/Transform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author David Gossow - dgossow@willowgarage.com 4 | */ 5 | 6 | import Vector3 from './Vector3.js'; 7 | import Quaternion from './Quaternion.js'; 8 | 9 | /** 10 | * A Transform in 3-space. Values are copied into this object. 11 | */ 12 | export default class Transform { 13 | /** 14 | * @param {Object} options 15 | * @param {Vector3} options.translation - The ROSLIB.Vector3 describing the translation. 16 | * @param {Quaternion} options.rotation - The ROSLIB.Quaternion describing the rotation. 17 | */ 18 | constructor(options) { 19 | // Copy the values into this object if they exist 20 | this.translation = new Vector3(options.translation); 21 | this.rotation = new Quaternion(options.rotation); 22 | } 23 | /** 24 | * Clone a copy of this transform. 25 | * 26 | * @returns {Transform} The cloned transform. 27 | */ 28 | clone() { 29 | return new Transform(this); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/math/Vector3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author David Gossow - dgossow@willowgarage.com 4 | */ 5 | 6 | import Quaternion from './Quaternion.js'; 7 | 8 | /** 9 | * A 3D vector. 10 | */ 11 | export default class Vector3 { 12 | /** 13 | * @param {Object} [options] 14 | * @param {number} [options.x=0] - The x value. 15 | * @param {number} [options.y=0] - The y value. 16 | * @param {number} [options.z=0] - The z value. 17 | */ 18 | constructor(options) { 19 | options = options || {}; 20 | this.x = options.x || 0; 21 | this.y = options.y || 0; 22 | this.z = options.z || 0; 23 | } 24 | /** 25 | * Set the values of this vector to the sum of itself and the given vector. 26 | * 27 | * @param {Vector3} v - The vector to add with. 28 | */ 29 | add(v) { 30 | this.x += v.x; 31 | this.y += v.y; 32 | this.z += v.z; 33 | } 34 | /** 35 | * Set the values of this vector to the difference of itself and the given vector. 36 | * 37 | * @param {Vector3} v - The vector to subtract with. 38 | */ 39 | subtract(v) { 40 | this.x -= v.x; 41 | this.y -= v.y; 42 | this.z -= v.z; 43 | } 44 | /** 45 | * Multiply the given Quaternion with this vector. 46 | * 47 | * @param {Quaternion} q - The quaternion to multiply with. 48 | */ 49 | multiplyQuaternion(q) { 50 | var ix = q.w * this.x + q.y * this.z - q.z * this.y; 51 | var iy = q.w * this.y + q.z * this.x - q.x * this.z; 52 | var iz = q.w * this.z + q.x * this.y - q.y * this.x; 53 | var iw = -q.x * this.x - q.y * this.y - q.z * this.z; 54 | this.x = ix * q.w + iw * -q.x + iy * -q.z - iz * -q.y; 55 | this.y = iy * q.w + iw * -q.y + iz * -q.x - ix * -q.z; 56 | this.z = iz * q.w + iw * -q.z + ix * -q.y - iy * -q.x; 57 | } 58 | /** 59 | * Clone a copy of this vector. 60 | * 61 | * @returns {Vector3} The cloned vector. 62 | */ 63 | clone() { 64 | return new Vector3(this); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/math/index.js: -------------------------------------------------------------------------------- 1 | export { default as Pose } from './Pose.js'; 2 | export { default as Quaternion } from './Quaternion.js'; 3 | export { default as Transform } from './Transform.js'; 4 | export { default as Vector3 } from './Vector3.js'; 5 | -------------------------------------------------------------------------------- /src/tf/ROS2TFClient.js: -------------------------------------------------------------------------------- 1 | 2 | import Action from '../core/Action.js'; 3 | import Transform from '../math/Transform.js'; 4 | 5 | import Ros from '../core/Ros.js'; 6 | import {EventEmitter} from 'eventemitter3'; 7 | 8 | /** 9 | * A TF Client that listens to TFs from tf2_web_republisher. 10 | */ 11 | export default class ROS2TFClient extends EventEmitter { 12 | /** 13 | * @param {Object} options 14 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 15 | * @param {string} [options.fixedFrame=base_link] - The fixed frame. 16 | * @param {number} [options.angularThres=2.0] - The angular threshold for the TF republisher. 17 | * @param {number} [options.transThres=0.01] - The translation threshold for the TF republisher. 18 | * @param {number} [options.rate=10.0] - The rate for the TF republisher. 19 | * @param {number} [options.updateDelay=50] - The time (in ms) to wait after a new subscription 20 | * to update the TF republisher's list of TFs. 21 | * @param {number} [options.topicTimeout=2.0] - The timeout parameter for the TF republisher. 22 | * @param {string} [options.serverName="/tf2_web_republisher"] - The name of the tf2_web_republisher server. 23 | * @param {string} [options.repubServiceName="/republish_tfs"] - The name of the republish_tfs service (non groovy compatibility mode only). 24 | */ 25 | constructor(options) { 26 | super(); 27 | this.ros = options.ros; 28 | this.fixedFrame = options.fixedFrame || 'base_link'; 29 | this.angularThres = options.angularThres || 2.0; 30 | this.transThres = options.transThres || 0.01; 31 | this.rate = options.rate || 10.0; 32 | this.updateDelay = options.updateDelay || 50; 33 | const seconds = options.topicTimeout || 2.0; 34 | const secs = Math.floor(seconds); 35 | const nsecs = Math.floor((seconds - secs) * 1E9); 36 | this.topicTimeout = { 37 | secs: secs, 38 | nsecs: nsecs 39 | }; 40 | this.serverName = options.serverName || '/tf2_web_republisher'; 41 | this.goal_id = ''; 42 | this.frameInfos = {}; 43 | this.republisherUpdateRequested = false; 44 | this._subscribeCB = undefined; 45 | this._isDisposed = false; 46 | 47 | // Create an Action Client 48 | this.actionClient = new Action({ 49 | ros: options.ros, 50 | name: this.serverName, 51 | actionType: 'tf2_web_republisher_msgs/TFSubscription', 52 | }); 53 | 54 | } 55 | 56 | /** 57 | * Process the incoming TF message and send them out using the callback 58 | * functions. 59 | * 60 | * @param {Object} tf - The TF message from the server. 61 | */ 62 | processTFArray(tf) { 63 | let that = this; 64 | tf.transforms.forEach(function (transform) { 65 | let frameID = transform.child_frame_id; 66 | if (frameID[0] === '/') { 67 | frameID = frameID.substring(1); 68 | } 69 | const info = that.frameInfos[frameID]; 70 | if (info) { 71 | info.transform = new Transform({ 72 | translation: transform.transform.translation, 73 | rotation: transform.transform.rotation 74 | }); 75 | info.cbs.forEach(function (cb) { 76 | cb(info.transform); 77 | }); 78 | } 79 | }, this); 80 | } 81 | 82 | /** 83 | * Create and send a new goal (or service request) to the tf2_web_republisher 84 | * based on the current list of TFs. 85 | */ 86 | updateGoal() { 87 | const goalMessage = { 88 | source_frames: Object.keys(this.frameInfos), 89 | target_frame: this.fixedFrame, 90 | angular_thres: this.angularThres, 91 | trans_thres: this.transThres, 92 | rate: this.rate 93 | }; 94 | 95 | if (this.goal_id !== '') { 96 | this.actionClient.cancelGoal(this.goal_id); 97 | } 98 | this.currentGoal = goalMessage; 99 | 100 | const id = this.actionClient.sendGoal(goalMessage, 101 | (result) => { 102 | }, 103 | (feedback) => { 104 | this.processTFArray(feedback) 105 | }, 106 | ); 107 | if (typeof id === 'string') { 108 | this.goal_id = id; 109 | } 110 | 111 | this.republisherUpdateRequested = false; 112 | } 113 | 114 | /** 115 | * @callback subscribeCallback 116 | * @param {Transform} callback.transform - The transform data. 117 | */ 118 | /** 119 | * Subscribe to the given TF frame. 120 | * 121 | * @param {string} frameID - The TF frame to subscribe to. 122 | * @param {subscribeCallback} callback - Function with the following params: 123 | */ 124 | subscribe(frameID, callback) { 125 | // remove leading slash, if it's there 126 | if (frameID[0] === '/') { 127 | frameID = frameID.substring(1); 128 | } 129 | // if there is no callback registered for the given frame, create empty callback list 130 | if (!this.frameInfos[frameID]) { 131 | this.frameInfos[frameID] = { 132 | cbs: [] 133 | }; 134 | if (!this.republisherUpdateRequested) { 135 | setTimeout(this.updateGoal.bind(this), this.updateDelay); 136 | this.republisherUpdateRequested = true; 137 | } 138 | } 139 | 140 | // if we already have a transform, callback immediately 141 | else if (this.frameInfos[frameID].transform) { 142 | callback(this.frameInfos[frameID].transform); 143 | } 144 | this.frameInfos[frameID].cbs.push(callback); 145 | } 146 | 147 | /** 148 | * Unsubscribe from the given TF frame. 149 | * 150 | * @param {string} frameID - The TF frame to unsubscribe from. 151 | * @param {function} callback - The callback function to remove. 152 | */ 153 | unsubscribe(frameID, callback) { 154 | // remove leading slash, if it's there 155 | if (frameID[0] === '/') { 156 | frameID = frameID.substring(1); 157 | } 158 | const info = this.frameInfos[frameID]; 159 | for (var cbs = (info && info.cbs) || [], idx = cbs.length; idx--;) { 160 | if (cbs[idx] === callback) { 161 | cbs.splice(idx, 1); 162 | } 163 | } 164 | if (!callback || cbs.length === 0) { 165 | delete this.frameInfos[frameID]; 166 | } 167 | } 168 | 169 | /** 170 | * Unsubscribe and unadvertise all topics associated with this TFClient. 171 | */ 172 | dispose() { 173 | this._isDisposed = true; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/tf/TFClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author David Gossow - dgossow@willowgarage.com 4 | */ 5 | 6 | import ActionClient from '../actionlib/ActionClient.js'; 7 | import Goal from '../actionlib/Goal.js'; 8 | 9 | import Service from '../core/Service.js'; 10 | import Topic from '../core/Topic.js'; 11 | 12 | import Transform from '../math/Transform.js'; 13 | 14 | import Ros from '../core/Ros.js'; 15 | import { EventEmitter } from 'eventemitter3'; 16 | 17 | /** 18 | * A TF Client that listens to TFs from tf2_web_republisher. 19 | */ 20 | export default class TFClient extends EventEmitter { 21 | /** @type {Goal|false} */ 22 | currentGoal = false; 23 | /** @type {Topic|false} */ 24 | currentTopic = false; 25 | frameInfos = {}; 26 | republisherUpdateRequested = false; 27 | /** @type {((tf: any) => any) | undefined} */ 28 | _subscribeCB = undefined; 29 | _isDisposed = false; 30 | /** 31 | * @param {Object} options 32 | * @param {Ros} options.ros - The ROSLIB.Ros connection handle. 33 | * @param {string} [options.fixedFrame=base_link] - The fixed frame. 34 | * @param {number} [options.angularThres=2.0] - The angular threshold for the TF republisher. 35 | * @param {number} [options.transThres=0.01] - The translation threshold for the TF republisher. 36 | * @param {number} [options.rate=10.0] - The rate for the TF republisher. 37 | * @param {number} [options.updateDelay=50] - The time (in ms) to wait after a new subscription 38 | * to update the TF republisher's list of TFs. 39 | * @param {number} [options.topicTimeout=2.0] - The timeout parameter for the TF republisher. 40 | * @param {string} [options.serverName="/tf2_web_republisher"] - The name of the tf2_web_republisher server. 41 | * @param {string} [options.repubServiceName="/republish_tfs"] - The name of the republish_tfs service (non groovy compatibility mode only). 42 | */ 43 | constructor(options) { 44 | super(); 45 | this.ros = options.ros; 46 | this.fixedFrame = options.fixedFrame || 'base_link'; 47 | this.angularThres = options.angularThres || 2.0; 48 | this.transThres = options.transThres || 0.01; 49 | this.rate = options.rate || 10.0; 50 | this.updateDelay = options.updateDelay || 50; 51 | var seconds = options.topicTimeout || 2.0; 52 | var secs = Math.floor(seconds); 53 | var nsecs = Math.floor((seconds - secs) * 1000000000); 54 | this.topicTimeout = { 55 | secs: secs, 56 | nsecs: nsecs 57 | }; 58 | this.serverName = options.serverName || '/tf2_web_republisher'; 59 | this.repubServiceName = options.repubServiceName || '/republish_tfs'; 60 | 61 | // Create an Action Client 62 | this.actionClient = new ActionClient({ 63 | ros: options.ros, 64 | serverName: this.serverName, 65 | actionName: 'tf2_web_republisher/TFSubscriptionAction', 66 | omitStatus: true, 67 | omitResult: true 68 | }); 69 | 70 | // Create a Service Client 71 | this.serviceClient = new Service({ 72 | ros: options.ros, 73 | name: this.repubServiceName, 74 | serviceType: 'tf2_web_republisher/RepublishTFs' 75 | }); 76 | } 77 | /** 78 | * Process the incoming TF message and send them out using the callback 79 | * functions. 80 | * 81 | * @param {Object} tf - The TF message from the server. 82 | */ 83 | processTFArray(tf) { 84 | tf.transforms.forEach((transform) => { 85 | var frameID = transform.child_frame_id; 86 | if (frameID[0] === '/') { 87 | frameID = frameID.substring(1); 88 | } 89 | var info = this.frameInfos[frameID]; 90 | if (info) { 91 | info.transform = new Transform({ 92 | translation: transform.transform.translation, 93 | rotation: transform.transform.rotation 94 | }); 95 | info.cbs.forEach((cb) => { 96 | cb(info.transform); 97 | }); 98 | } 99 | }, this); 100 | } 101 | /** 102 | * Create and send a new goal (or service request) to the tf2_web_republisher 103 | * based on the current list of TFs. 104 | */ 105 | updateGoal() { 106 | var goalMessage = { 107 | source_frames: Object.keys(this.frameInfos), 108 | target_frame: this.fixedFrame, 109 | angular_thres: this.angularThres, 110 | trans_thres: this.transThres, 111 | rate: this.rate 112 | }; 113 | 114 | // if we're running in groovy compatibility mode (the default) 115 | // then use the action interface to tf2_web_republisher 116 | if (this.ros.groovyCompatibility) { 117 | if (this.currentGoal) { 118 | this.currentGoal.cancel(); 119 | } 120 | this.currentGoal = new Goal({ 121 | actionClient: this.actionClient, 122 | goalMessage: goalMessage 123 | }); 124 | 125 | this.currentGoal.on('feedback', this.processTFArray.bind(this)); 126 | this.currentGoal.send(); 127 | } else { 128 | // otherwise, use the service interface 129 | // The service interface has the same parameters as the action, 130 | // plus the timeout 131 | goalMessage.timeout = this.topicTimeout; 132 | this.serviceClient.callService(goalMessage, this.processResponse.bind(this)); 133 | } 134 | 135 | this.republisherUpdateRequested = false; 136 | } 137 | /** 138 | * Process the service response and subscribe to the tf republisher 139 | * topic. 140 | * 141 | * @param {Object} response - The service response containing the topic name. 142 | */ 143 | processResponse(response) { 144 | // Do not setup a topic subscription if already disposed. Prevents a race condition where 145 | // The dispose() function is called before the service call receives a response. 146 | if (this._isDisposed) { 147 | return; 148 | } 149 | 150 | // if we subscribed to a topic before, unsubscribe so 151 | // the republisher stops publishing it 152 | if (this.currentTopic) { 153 | this.currentTopic.unsubscribe(this._subscribeCB); 154 | } 155 | 156 | this.currentTopic = new Topic({ 157 | ros: this.ros, 158 | name: response.topic_name, 159 | messageType: 'tf2_web_republisher/TFArray' 160 | }); 161 | this._subscribeCB = this.processTFArray.bind(this); 162 | this.currentTopic.subscribe(this._subscribeCB); 163 | } 164 | /** 165 | * @callback subscribeCallback 166 | * @param {Transform} callback.transform - The transform data. 167 | */ 168 | /** 169 | * Subscribe to the given TF frame. 170 | * 171 | * @param {string} frameID - The TF frame to subscribe to. 172 | * @param {subscribeCallback} callback - Function with the following params: 173 | */ 174 | subscribe(frameID, callback) { 175 | // remove leading slash, if it's there 176 | if (frameID[0] === '/') { 177 | frameID = frameID.substring(1); 178 | } 179 | // if there is no callback registered for the given frame, create empty callback list 180 | if (!this.frameInfos[frameID]) { 181 | this.frameInfos[frameID] = { 182 | cbs: [] 183 | }; 184 | if (!this.republisherUpdateRequested) { 185 | setTimeout(this.updateGoal.bind(this), this.updateDelay); 186 | this.republisherUpdateRequested = true; 187 | } 188 | } 189 | 190 | // if we already have a transform, callback immediately 191 | else if (this.frameInfos[frameID].transform) { 192 | callback(this.frameInfos[frameID].transform); 193 | } 194 | this.frameInfos[frameID].cbs.push(callback); 195 | } 196 | /** 197 | * Unsubscribe from the given TF frame. 198 | * 199 | * @param {string} frameID - The TF frame to unsubscribe from. 200 | * @param {function} callback - The callback function to remove. 201 | */ 202 | unsubscribe(frameID, callback) { 203 | // remove leading slash, if it's there 204 | if (frameID[0] === '/') { 205 | frameID = frameID.substring(1); 206 | } 207 | var info = this.frameInfos[frameID]; 208 | for (var cbs = (info && info.cbs) || [], idx = cbs.length; idx--; ) { 209 | if (cbs[idx] === callback) { 210 | cbs.splice(idx, 1); 211 | } 212 | } 213 | if (!callback || cbs.length === 0) { 214 | delete this.frameInfos[frameID]; 215 | } 216 | } 217 | /** 218 | * Unsubscribe and unadvertise all topics associated with this TFClient. 219 | */ 220 | dispose() { 221 | this._isDisposed = true; 222 | this.actionClient.dispose(); 223 | if (this.currentTopic) { 224 | this.currentTopic.unsubscribe(this._subscribeCB); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/tf/index.js: -------------------------------------------------------------------------------- 1 | export { default as TFClient } from './TFClient.js'; 2 | export { default as ROS2TFClient } from './ROS2TFClient.js'; 3 | -------------------------------------------------------------------------------- /src/urdf/UrdfBox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import Vector3 from '../math/Vector3.js'; 8 | import * as UrdfTypes from './UrdfTypes.js'; 9 | 10 | /** 11 | * A Box element in a URDF. 12 | */ 13 | export default class UrdfBox { 14 | /** @type {Vector3 | null} */ 15 | dimension; 16 | /** 17 | * @param {Object} options 18 | * @param {Element} options.xml - The XML element to parse. 19 | */ 20 | constructor(options) { 21 | this.type = UrdfTypes.URDF_BOX; 22 | 23 | // Parse the xml string 24 | var xyz = options.xml.getAttribute('size')?.split(' '); 25 | if (xyz) { 26 | this.dimension = new Vector3({ 27 | x: parseFloat(xyz[0]), 28 | y: parseFloat(xyz[1]), 29 | z: parseFloat(xyz[2]) 30 | }); 31 | } else { 32 | this.dimension = null; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/urdf/UrdfColor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | /** 8 | * A Color element in a URDF. 9 | */ 10 | export default class UrdfColor { 11 | /** 12 | * @param {Object} options 13 | * @param {Element} options.xml - The XML element to parse. 14 | */ 15 | constructor(options) { 16 | // Parse the xml string 17 | var rgba = options.xml.getAttribute('rgba')?.split(' '); 18 | if (rgba) { 19 | this.r = parseFloat(rgba[0]); 20 | this.g = parseFloat(rgba[1]); 21 | this.b = parseFloat(rgba[2]); 22 | this.a = parseFloat(rgba[3]); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/urdf/UrdfCylinder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import * as UrdfTypes from './UrdfTypes.js'; 8 | 9 | /** 10 | * A Cylinder element in a URDF. 11 | */ 12 | export default class UrdfCylinder { 13 | /** 14 | * @param {Object} options 15 | * @param {Element} options.xml - The XML element to parse. 16 | */ 17 | constructor(options) { 18 | this.type = UrdfTypes.URDF_CYLINDER; 19 | // @ts-expect-error -- possibly null 20 | this.length = parseFloat(options.xml.getAttribute('length')); 21 | // @ts-expect-error -- possibly null 22 | this.radius = parseFloat(options.xml.getAttribute('radius')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/urdf/UrdfJoint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author David V. Lu!! - davidvlu@gmail.com 4 | */ 5 | 6 | import Pose from '../math/Pose.js'; 7 | import Vector3 from '../math/Vector3.js'; 8 | import Quaternion from '../math/Quaternion.js'; 9 | 10 | /** 11 | * A Joint element in a URDF. 12 | */ 13 | export default class UrdfJoint { 14 | /** 15 | * @param {Object} options 16 | * @param {Element} options.xml - The XML element to parse. 17 | */ 18 | constructor(options) { 19 | this.name = options.xml.getAttribute('name'); 20 | this.type = options.xml.getAttribute('type'); 21 | 22 | var parents = options.xml.getElementsByTagName('parent'); 23 | if (parents.length > 0) { 24 | this.parent = parents[0].getAttribute('link'); 25 | } 26 | 27 | var children = options.xml.getElementsByTagName('child'); 28 | if (children.length > 0) { 29 | this.child = children[0].getAttribute('link'); 30 | } 31 | 32 | var limits = options.xml.getElementsByTagName('limit'); 33 | if (limits.length > 0) { 34 | this.minval = parseFloat(limits[0].getAttribute('lower') || 'NaN'); 35 | this.maxval = parseFloat(limits[0].getAttribute('upper') || 'NaN'); 36 | } 37 | 38 | // Origin 39 | var origins = options.xml.getElementsByTagName('origin'); 40 | if (origins.length === 0) { 41 | // use the identity as the default 42 | this.origin = new Pose(); 43 | } else { 44 | // Check the XYZ 45 | var xyzValue = origins[0].getAttribute('xyz'); 46 | var position = new Vector3(); 47 | if (xyzValue) { 48 | var xyz = xyzValue.split(' '); 49 | position = new Vector3({ 50 | x: parseFloat(xyz[0]), 51 | y: parseFloat(xyz[1]), 52 | z: parseFloat(xyz[2]) 53 | }); 54 | } 55 | 56 | // Check the RPY 57 | var rpyValue = origins[0].getAttribute('rpy'); 58 | var orientation = new Quaternion(); 59 | if (rpyValue) { 60 | var rpy = rpyValue.split(' '); 61 | // Convert from RPY 62 | var roll = parseFloat(rpy[0]); 63 | var pitch = parseFloat(rpy[1]); 64 | var yaw = parseFloat(rpy[2]); 65 | var phi = roll / 2.0; 66 | var the = pitch / 2.0; 67 | var psi = yaw / 2.0; 68 | var x = 69 | Math.sin(phi) * Math.cos(the) * Math.cos(psi) - 70 | Math.cos(phi) * Math.sin(the) * Math.sin(psi); 71 | var y = 72 | Math.cos(phi) * Math.sin(the) * Math.cos(psi) + 73 | Math.sin(phi) * Math.cos(the) * Math.sin(psi); 74 | var z = 75 | Math.cos(phi) * Math.cos(the) * Math.sin(psi) - 76 | Math.sin(phi) * Math.sin(the) * Math.cos(psi); 77 | var w = 78 | Math.cos(phi) * Math.cos(the) * Math.cos(psi) + 79 | Math.sin(phi) * Math.sin(the) * Math.sin(psi); 80 | 81 | orientation = new Quaternion({ 82 | x: x, 83 | y: y, 84 | z: z, 85 | w: w 86 | }); 87 | orientation.normalize(); 88 | } 89 | this.origin = new Pose({ 90 | position: position, 91 | orientation: orientation 92 | }); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/urdf/UrdfLink.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import UrdfVisual from './UrdfVisual.js'; 8 | 9 | /** 10 | * A Link element in a URDF. 11 | */ 12 | export default class UrdfLink { 13 | /** 14 | * @param {Object} options 15 | * @param {Element} options.xml - The XML element to parse. 16 | */ 17 | constructor(options) { 18 | this.name = options.xml.getAttribute('name'); 19 | this.visuals = []; 20 | var visuals = options.xml.getElementsByTagName('visual'); 21 | 22 | for (var i = 0; i < visuals.length; i++) { 23 | this.visuals.push( 24 | new UrdfVisual({ 25 | xml: visuals[i] 26 | }) 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/urdf/UrdfMaterial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import UrdfColor from './UrdfColor.js'; 8 | 9 | /** 10 | * A Material element in a URDF. 11 | */ 12 | export default class UrdfMaterial { 13 | /** @type {string | null} */ 14 | textureFilename = null; 15 | /** @type {UrdfColor | null} */ 16 | color = null; 17 | /** 18 | * @param {Object} options 19 | * @param {Element} options.xml - The XML element to parse. 20 | */ 21 | constructor(options) { 22 | 23 | this.name = options.xml.getAttribute('name'); 24 | 25 | // Texture 26 | var textures = options.xml.getElementsByTagName('texture'); 27 | if (textures.length > 0) { 28 | this.textureFilename = textures[0].getAttribute('filename'); 29 | } 30 | 31 | // Color 32 | var colors = options.xml.getElementsByTagName('color'); 33 | if (colors.length > 0) { 34 | // Parse the RBGA string 35 | this.color = new UrdfColor({ 36 | xml: colors[0] 37 | }); 38 | } 39 | } 40 | isLink() { 41 | return this.color === null && this.textureFilename === null; 42 | } 43 | assign(obj) { 44 | return Object.assign(this, obj); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/urdf/UrdfMesh.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import Vector3 from '../math/Vector3.js'; 8 | import * as UrdfTypes from './UrdfTypes.js'; 9 | 10 | /** 11 | * A Mesh element in a URDF. 12 | */ 13 | export default class UrdfMesh { 14 | /** @type {Vector3 | null} */ 15 | scale = null; 16 | /** 17 | * @param {Object} options 18 | * @param {Element} options.xml - The XML element to parse. 19 | */ 20 | constructor(options) { 21 | this.type = UrdfTypes.URDF_MESH; 22 | this.filename = options.xml.getAttribute('filename'); 23 | 24 | // Check for a scale 25 | var scale = options.xml.getAttribute('scale'); 26 | if (scale) { 27 | // Get the XYZ 28 | var xyz = scale.split(' '); 29 | this.scale = new Vector3({ 30 | x: parseFloat(xyz[0]), 31 | y: parseFloat(xyz[1]), 32 | z: parseFloat(xyz[2]) 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/urdf/UrdfModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import UrdfMaterial from './UrdfMaterial.js'; 8 | import UrdfLink from './UrdfLink.js'; 9 | import UrdfJoint from './UrdfJoint.js'; 10 | import { DOMParser, MIME_TYPE } from '@xmldom/xmldom'; 11 | 12 | // See https://developer.mozilla.org/docs/XPathResult#Constants 13 | var XPATH_FIRST_ORDERED_NODE_TYPE = 9; 14 | 15 | /** 16 | * A URDF Model can be used to parse a given URDF into the appropriate elements. 17 | */ 18 | export default class UrdfModel { 19 | materials = {}; 20 | links = {}; 21 | joints = {}; 22 | /** 23 | * @param {Object} options 24 | * @param {Element | null} [options.xml] - The XML element to parse. 25 | * @param {string} [options.string] - The XML element to parse as a string. 26 | */ 27 | constructor(options) { 28 | var xmlDoc = options.xml; 29 | var string = options.string; 30 | 31 | // Check if we are using a string or an XML element 32 | if (string) { 33 | // Parse the string 34 | var parser = new DOMParser(); 35 | xmlDoc = parser.parseFromString(string, MIME_TYPE.XML_TEXT).documentElement; 36 | } 37 | if (!xmlDoc) { 38 | throw new Error('No URDF document parsed!'); 39 | } 40 | 41 | // Initialize the model with the given XML node. 42 | // Get the robot tag 43 | var robotXml = xmlDoc; 44 | 45 | // Get the robot name 46 | this.name = robotXml.getAttribute('name'); 47 | 48 | // Parse all the visual elements we need 49 | for (var nodes = robotXml.childNodes, i = 0; i < nodes.length; i++) { 50 | /** @type {Element} */ 51 | // @ts-expect-error -- unknown why this doesn't work properly. 52 | var node = nodes[i]; 53 | if (node.tagName === 'material') { 54 | var material = new UrdfMaterial({ 55 | xml: node 56 | }); 57 | // Make sure this is unique 58 | if (this.materials[material.name] !== void 0) { 59 | if (this.materials[material.name].isLink()) { 60 | this.materials[material.name].assign(material); 61 | } else { 62 | console.warn('Material ' + material.name + 'is not unique.'); 63 | } 64 | } else { 65 | this.materials[material.name] = material; 66 | } 67 | } else if (node.tagName === 'link') { 68 | var link = new UrdfLink({ 69 | xml: node 70 | }); 71 | // Make sure this is unique 72 | if (this.links[link.name] !== void 0) { 73 | console.warn('Link ' + link.name + ' is not unique.'); 74 | } else { 75 | // Check for a material 76 | for (var j = 0; j < link.visuals.length; j++) { 77 | var mat = link.visuals[j].material; 78 | if (mat !== null && mat.name) { 79 | if (this.materials[mat.name] !== void 0) { 80 | link.visuals[j].material = this.materials[mat.name]; 81 | } else { 82 | this.materials[mat.name] = mat; 83 | } 84 | } 85 | } 86 | 87 | // Add the link 88 | this.links[link.name] = link; 89 | } 90 | } else if (node.tagName === 'joint') { 91 | var joint = new UrdfJoint({ 92 | xml: node 93 | }); 94 | this.joints[joint.name] = joint; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/urdf/UrdfSphere.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import * as UrdfTypes from './UrdfTypes.js'; 8 | 9 | /** 10 | * A Sphere element in a URDF. 11 | */ 12 | export default class UrdfSphere { 13 | /** 14 | * @param {Object} options 15 | * @param {Element} options.xml - The XML element to parse. 16 | */ 17 | constructor(options) { 18 | this.type = UrdfTypes.URDF_SPHERE; 19 | this.radius = parseFloat(options.xml.getAttribute('radius') || 'NaN'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/urdf/UrdfTypes.js: -------------------------------------------------------------------------------- 1 | export const URDF_SPHERE = 0; 2 | export const URDF_BOX = 1; 3 | export const URDF_CYLINDER = 2; 4 | export const URDF_MESH = 3; 5 | -------------------------------------------------------------------------------- /src/urdf/UrdfVisual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Benjamin Pitzer - ben.pitzer@gmail.com 4 | * @author Russell Toris - rctoris@wpi.edu 5 | */ 6 | 7 | import Pose from '../math/Pose.js'; 8 | import Vector3 from '../math/Vector3.js'; 9 | import Quaternion from '../math/Quaternion.js'; 10 | 11 | import UrdfCylinder from './UrdfCylinder.js'; 12 | import UrdfBox from './UrdfBox.js'; 13 | import UrdfMaterial from './UrdfMaterial.js'; 14 | import UrdfMesh from './UrdfMesh.js'; 15 | import UrdfSphere from './UrdfSphere.js'; 16 | 17 | /** 18 | * A Visual element in a URDF. 19 | */ 20 | export default class UrdfVisual { 21 | /** @type {Pose | null} */ 22 | origin = null; 23 | /** @type {UrdfMesh | UrdfSphere | UrdfBox | UrdfCylinder | null} */ 24 | geometry = null; 25 | /** @type {UrdfMaterial | null} */ 26 | material = null; 27 | /** 28 | * @param {Object} options 29 | * @param {Element} options.xml - The XML element to parse. 30 | */ 31 | constructor(options) { 32 | var xml = options.xml; 33 | this.name = options.xml.getAttribute('name'); 34 | 35 | // Origin 36 | var origins = xml.getElementsByTagName('origin'); 37 | if (origins.length === 0) { 38 | // use the identity as the default 39 | this.origin = new Pose(); 40 | } else { 41 | // Check the XYZ 42 | var xyzValue = origins[0].getAttribute('xyz'); 43 | var position = new Vector3(); 44 | if (xyzValue) { 45 | var xyz = xyzValue.split(' '); 46 | position = new Vector3({ 47 | x: parseFloat(xyz[0]), 48 | y: parseFloat(xyz[1]), 49 | z: parseFloat(xyz[2]) 50 | }); 51 | } 52 | 53 | // Check the RPY 54 | var rpyValue = origins[0].getAttribute('rpy'); 55 | var orientation = new Quaternion(); 56 | if (rpyValue) { 57 | var rpy = rpyValue.split(' '); 58 | // Convert from RPY 59 | var roll = parseFloat(rpy[0]); 60 | var pitch = parseFloat(rpy[1]); 61 | var yaw = parseFloat(rpy[2]); 62 | var phi = roll / 2.0; 63 | var the = pitch / 2.0; 64 | var psi = yaw / 2.0; 65 | var x = 66 | Math.sin(phi) * Math.cos(the) * Math.cos(psi) - 67 | Math.cos(phi) * Math.sin(the) * Math.sin(psi); 68 | var y = 69 | Math.cos(phi) * Math.sin(the) * Math.cos(psi) + 70 | Math.sin(phi) * Math.cos(the) * Math.sin(psi); 71 | var z = 72 | Math.cos(phi) * Math.cos(the) * Math.sin(psi) - 73 | Math.sin(phi) * Math.sin(the) * Math.cos(psi); 74 | var w = 75 | Math.cos(phi) * Math.cos(the) * Math.cos(psi) + 76 | Math.sin(phi) * Math.sin(the) * Math.sin(psi); 77 | 78 | orientation = new Quaternion({ 79 | x: x, 80 | y: y, 81 | z: z, 82 | w: w 83 | }); 84 | orientation.normalize(); 85 | } 86 | this.origin = new Pose({ 87 | position: position, 88 | orientation: orientation 89 | }); 90 | } 91 | 92 | // Geometry 93 | var geoms = xml.getElementsByTagName('geometry'); 94 | if (geoms.length > 0) { 95 | var geom = geoms[0]; 96 | var shape = null; 97 | // Check for the shape 98 | for (var i = 0; i < geom.childNodes.length; i++) { 99 | /** @type {Element} */ 100 | // @ts-expect-error -- unknown why this doesn't work properly. 101 | var node = geom.childNodes[i]; 102 | if (node.nodeType === 1) { 103 | shape = node; 104 | break; 105 | } 106 | } 107 | if (shape) { 108 | // Check the type 109 | var type = shape.nodeName; 110 | if (type === 'sphere') { 111 | this.geometry = new UrdfSphere({ 112 | xml: shape 113 | }); 114 | } else if (type === 'box') { 115 | this.geometry = new UrdfBox({ 116 | xml: shape 117 | }); 118 | } else if (type === 'cylinder') { 119 | this.geometry = new UrdfCylinder({ 120 | xml: shape 121 | }); 122 | } else if (type === 'mesh') { 123 | this.geometry = new UrdfMesh({ 124 | xml: shape 125 | }); 126 | } else { 127 | console.warn('Unknown geometry type ' + type); 128 | } 129 | } 130 | } 131 | 132 | // Material 133 | var materials = xml.getElementsByTagName('material'); 134 | if (materials.length > 0) { 135 | this.material = new UrdfMaterial({ 136 | xml: materials[0] 137 | }); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/urdf/index.js: -------------------------------------------------------------------------------- 1 | export { default as UrdfBox } from './UrdfBox.js'; 2 | export { default as UrdfColor } from './UrdfColor.js'; 3 | export { default as UrdfCylinder } from './UrdfCylinder.js'; 4 | export { default as UrdfLink } from './UrdfLink.js'; 5 | export { default as UrdfMaterial } from './UrdfMaterial.js'; 6 | export { default as UrdfMesh } from './UrdfMesh.js'; 7 | export { default as UrdfModel } from './UrdfModel.js'; 8 | export { default as UrdfSphere } from './UrdfSphere.js'; 9 | export { default as UrdfVisual } from './UrdfVisual.js'; 10 | 11 | export * from './UrdfTypes.js'; 12 | -------------------------------------------------------------------------------- /src/util/cborTypedArrayTags.js: -------------------------------------------------------------------------------- 1 | var UPPER32 = Math.pow(2, 32); 2 | 3 | var warnedPrecision = false; 4 | function warnPrecision() { 5 | if (!warnedPrecision) { 6 | warnedPrecision = true; 7 | console.warn( 8 | 'CBOR 64-bit integer array values may lose precision. No further warnings.' 9 | ); 10 | } 11 | } 12 | 13 | /** 14 | * Unpack 64-bit unsigned integer from byte array. 15 | * @param {Uint8Array} bytes 16 | */ 17 | function decodeUint64LE(bytes) { 18 | warnPrecision(); 19 | 20 | var byteLen = bytes.byteLength; 21 | var offset = bytes.byteOffset; 22 | var arrLen = byteLen / 8; 23 | 24 | var buffer = bytes.buffer.slice(offset, offset + byteLen); 25 | var uint32View = new Uint32Array(buffer); 26 | 27 | var arr = new Array(arrLen); 28 | for (var i = 0; i < arrLen; i++) { 29 | var si = i * 2; 30 | var lo = uint32View[si]; 31 | var hi = uint32View[si + 1]; 32 | arr[i] = lo + UPPER32 * hi; 33 | } 34 | 35 | return arr; 36 | } 37 | 38 | /** 39 | * Unpack 64-bit signed integer from byte array. 40 | * @param {Uint8Array} bytes 41 | */ 42 | function decodeInt64LE(bytes) { 43 | warnPrecision(); 44 | 45 | var byteLen = bytes.byteLength; 46 | var offset = bytes.byteOffset; 47 | var arrLen = byteLen / 8; 48 | 49 | var buffer = bytes.buffer.slice(offset, offset + byteLen); 50 | var uint32View = new Uint32Array(buffer); 51 | var int32View = new Int32Array(buffer); 52 | 53 | var arr = new Array(arrLen); 54 | for (var i = 0; i < arrLen; i++) { 55 | var si = i * 2; 56 | var lo = uint32View[si]; 57 | var hi = int32View[si + 1]; 58 | arr[i] = lo + UPPER32 * hi; 59 | } 60 | 61 | return arr; 62 | } 63 | 64 | /** 65 | * Unpack typed array from byte array. 66 | * @param {Uint8Array} bytes 67 | * @param {ArrayConstructor} ArrayType - Desired output array type 68 | */ 69 | function decodeNativeArray(bytes, ArrayType) { 70 | var byteLen = bytes.byteLength; 71 | var offset = bytes.byteOffset; 72 | var buffer = bytes.buffer.slice(offset, offset + byteLen); 73 | return new ArrayType(buffer); 74 | } 75 | 76 | /** 77 | * Supports a subset of draft CBOR typed array tags: 78 | * 79 | * 80 | * Only supports little-endian tags for now. 81 | */ 82 | var nativeArrayTypes = { 83 | 64: Uint8Array, 84 | 69: Uint16Array, 85 | 70: Uint32Array, 86 | 72: Int8Array, 87 | 77: Int16Array, 88 | 78: Int32Array, 89 | 85: Float32Array, 90 | 86: Float64Array 91 | }; 92 | 93 | /** 94 | * We can also decode 64-bit integer arrays, since ROS has these types. 95 | */ 96 | var conversionArrayTypes = { 97 | 71: decodeUint64LE, 98 | 79: decodeInt64LE 99 | }; 100 | 101 | /** 102 | * Handle CBOR typed array tags during decoding. 103 | * @param {Uint8Array} data 104 | * @param {Number} tag 105 | */ 106 | export default function cborTypedArrayTagger(data, tag) { 107 | if (tag in nativeArrayTypes) { 108 | var arrayType = nativeArrayTypes[tag]; 109 | return decodeNativeArray(data, arrayType); 110 | } 111 | if (tag in conversionArrayTypes) { 112 | return conversionArrayTypes[tag](data); 113 | } 114 | return data; 115 | } 116 | -------------------------------------------------------------------------------- /src/util/decompressPng.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Ramon Wijnands - rayman747@hotmail.com 4 | */ 5 | 6 | import pngparse from 'pngparse'; 7 | 8 | /** 9 | * @callback decompressPngCallback 10 | * @param data - The uncompressed data. 11 | */ 12 | /** 13 | * If a message was compressed as a PNG image (a compression hack since 14 | * gzipping over WebSockets * is not supported yet), this function decodes 15 | * the "image" as a Base64 string. 16 | * 17 | * @private 18 | * @param data - An object containing the PNG data. 19 | * @param {decompressPngCallback} callback - Function with the following params: 20 | */ 21 | export default function decompressPng(data, callback) { 22 | var buffer = new Buffer(data, 'base64'); 23 | 24 | pngparse.parse(buffer, function (err, data) { 25 | if (err) { 26 | console.warn('Cannot process PNG encoded message '); 27 | } else { 28 | var jsonData = data.data.toString(); 29 | callback(JSON.parse(jsonData)); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/util/shim/decompressPng.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * @author Graeme Yeates - github.com/megawac 4 | */ 5 | 6 | /** 7 | * @callback decompressPngCallback 8 | * @param data - The uncompressed data. 9 | */ 10 | /** 11 | * If a message was compressed as a PNG image (a compression hack since 12 | * gzipping over WebSockets * is not supported yet), this function places the 13 | * "image" in a canvas element then decodes the * "image" as a Base64 string. 14 | * 15 | * @private 16 | * @param data - An object containing the PNG data. 17 | * @param {decompressPngCallback} callback - Function with the following params: 18 | */ 19 | export default function decompressPng(data, callback) { 20 | // Uncompresses the data before sending it through (use image/canvas to do so). 21 | var image = new Image(); 22 | // When the image loads, extracts the raw data (JSON message). 23 | image.onload = function () { 24 | // Creates a local canvas to draw on. 25 | var canvas = document.createElement('canvas'); 26 | var context = canvas.getContext('2d'); 27 | 28 | if (!context) { 29 | throw new Error('Failed to create Canvas context!'); 30 | } 31 | 32 | // Sets width and height. 33 | canvas.width = image.width; 34 | canvas.height = image.height; 35 | 36 | // Prevents anti-aliasing and loosing data 37 | context.imageSmoothingEnabled = false; 38 | 39 | // Puts the data into the image. 40 | context.drawImage(image, 0, 0); 41 | // Grabs the raw, uncompressed data. 42 | var imageData = context.getImageData(0, 0, image.width, image.height).data; 43 | 44 | // Constructs the JSON. 45 | var jsonData = ''; 46 | for (var i = 0; i < imageData.length; i += 4) { 47 | // RGB 48 | jsonData += String.fromCharCode( 49 | imageData[i], 50 | imageData[i + 1], 51 | imageData[i + 2] 52 | ); 53 | } 54 | callback(JSON.parse(jsonData)); 55 | }; 56 | // Sends the image data to load. 57 | image.src = 'data:image/png;base64,' + data; 58 | } 59 | -------------------------------------------------------------------------------- /test/build.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 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 | set -e 18 | 19 | pushd "$(dirname "$0")" > /dev/null 20 | 21 | bash examples/setup_examples.bash 22 | 23 | echo -e "\e[1m\e[35mrostopic list\e[0m" 24 | rostopic list 25 | echo -e "\e[1m\e[35mnpm install\e[0m" 26 | npm install 27 | echo -e "\e[1m\e[35mnpm run build\e[0m" 28 | npm run build 29 | echo -e "\e[1m\e[35mnpm test\e[0m" 30 | npm test 31 | -------------------------------------------------------------------------------- /test/cbor.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import CBOR from 'cbor-js'; 3 | import cborTypedArrayTagger from '../src/util/cborTypedArrayTags.js'; 4 | 5 | /** Convert hex string to ArrayBuffer. */ 6 | function hexToBuffer(hex) { 7 | var tokens = hex.match(/[0-9a-fA-F]{2}/gi); 8 | var arr = tokens.map(function(t) { 9 | return parseInt(t, 16); 10 | }); 11 | return new Uint8Array(arr).buffer; 12 | } 13 | 14 | 15 | describe('CBOR Typed Array Tagger', function() { 16 | 17 | it('should convert tagged Uint16Array', function() { 18 | var data = hexToBuffer('d84546010002000300'); 19 | var msg = CBOR.decode(data, cborTypedArrayTagger); 20 | 21 | expect(msg).to.be.a('Uint16Array'); 22 | expect(msg).to.have.lengthOf(3); 23 | expect(msg[0]).to.equal(1); 24 | expect(msg[1]).to.equal(2); 25 | expect(msg[2]).to.equal(3); 26 | }); 27 | 28 | it('should convert tagged Uint32Array', function() { 29 | var data = hexToBuffer('d8464c010000000200000003000000'); 30 | var msg = CBOR.decode(data, cborTypedArrayTagger); 31 | 32 | expect(msg).to.be.a('Uint32Array'); 33 | expect(msg).to.have.lengthOf(3); 34 | expect(msg[0]).to.equal(1); 35 | expect(msg[1]).to.equal(2); 36 | expect(msg[2]).to.equal(3); 37 | }); 38 | 39 | it('should convert tagged Uint64Array', function() { 40 | var data = hexToBuffer('d8475818010000000000000002000000000000000300000000000000'); 41 | var msg = CBOR.decode(data, cborTypedArrayTagger); 42 | 43 | expect(msg).to.be.a('Array'); 44 | expect(msg).to.have.lengthOf(3); 45 | expect(msg[0]).to.equal(1); 46 | expect(msg[1]).to.equal(2); 47 | expect(msg[2]).to.equal(3); 48 | }); 49 | 50 | it('should convert tagged Int8Array', function() { 51 | var data = hexToBuffer('d8484301fe03'); 52 | var msg = CBOR.decode(data, cborTypedArrayTagger); 53 | 54 | expect(msg).to.be.a('Int8Array'); 55 | expect(msg).to.have.lengthOf(3); 56 | expect(msg[0]).to.equal(1); 57 | expect(msg[1]).to.equal(-2); 58 | expect(msg[2]).to.equal(3); 59 | }); 60 | 61 | it('should convert tagged Int16Array', function() { 62 | var data = hexToBuffer('d84d460100feff0300'); 63 | var msg = CBOR.decode(data, cborTypedArrayTagger); 64 | 65 | expect(msg).to.be.a('Int16Array'); 66 | expect(msg).to.have.lengthOf(3); 67 | expect(msg[0]).to.equal(1); 68 | expect(msg[1]).to.equal(-2); 69 | expect(msg[2]).to.equal(3); 70 | }); 71 | 72 | it('should convert tagged Int32Array', function() { 73 | var data = hexToBuffer('d84e4c01000000feffffff03000000'); 74 | var msg = CBOR.decode(data, cborTypedArrayTagger); 75 | 76 | expect(msg).to.be.a('Int32Array'); 77 | expect(msg).to.have.lengthOf(3); 78 | expect(msg[0]).to.equal(1); 79 | expect(msg[1]).to.equal(-2); 80 | expect(msg[2]).to.equal(3); 81 | }); 82 | 83 | it('should convert tagged Int64Array', function() { 84 | var data = hexToBuffer('d84f58180100000000000000feffffffffffffff0300000000000000'); 85 | var msg = CBOR.decode(data, cborTypedArrayTagger); 86 | 87 | expect(msg).to.be.a('Array'); 88 | expect(msg).to.have.lengthOf(3); 89 | expect(msg[0]).to.equal(1); 90 | expect(msg[1]).to.equal(-2); 91 | expect(msg[2]).to.equal(3); 92 | }); 93 | 94 | it('should convert tagged Float32Array', function() { 95 | var data = hexToBuffer('d8554ccdcc8c3fcdcc0cc033335340'); 96 | var msg = CBOR.decode(data, cborTypedArrayTagger); 97 | 98 | expect(msg).to.be.a('Float32Array'); 99 | expect(msg).to.have.lengthOf(3); 100 | expect(msg[0]).to.be.closeTo(1.1, 1e-5); 101 | expect(msg[1]).to.be.closeTo(-2.2, 1e-5); 102 | expect(msg[2]).to.be.closeTo(3.3, 1e-5); 103 | }); 104 | 105 | it('should convert tagged Float64Array', function() { 106 | var data = hexToBuffer('d85658189a9999999999f13f9a999999999901c06666666666660a40'); 107 | var msg = CBOR.decode(data, cborTypedArrayTagger); 108 | 109 | expect(msg).to.be.a('Float64Array'); 110 | expect(msg).to.have.lengthOf(3); 111 | expect(msg[0]).to.be.closeTo(1.1, 1e-5); 112 | expect(msg[1]).to.be.closeTo(-2.2, 1e-5); 113 | expect(msg[2]).to.be.closeTo(3.3, 1e-5); 114 | }); 115 | 116 | it('should be able to unpack two typed arrays', function() { 117 | var data = hexToBuffer('82d8484308fe05d84d460100feff0300'); 118 | var msg = CBOR.decode(data, cborTypedArrayTagger); 119 | 120 | expect(msg).to.be.a('Array'); 121 | expect(msg).to.have.lengthOf(2); 122 | expect(msg[0][0]).to.equal(8); 123 | expect(msg[0][1]).to.equal(-2); 124 | expect(msg[0][2]).to.equal(5); 125 | expect(msg[1][0]).to.equal(1); 126 | expect(msg[1][1]).to.equal(-2); 127 | expect(msg[1][2]).to.equal(3); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/cdn-import.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import '../dist/RosLib.umd.cjs'; 3 | import { readFileSync } from 'fs'; 4 | import path from 'path'; 5 | 6 | describe('Using as if imported from a CDN', () => { 7 | it('Adds itself to the global namespace', () => { 8 | expect(globalThis.ROSLIB).toBeTruthy(); 9 | }) 10 | it('Does not include EventEmitter in the bundle', () => { 11 | // Read the bundled output of the file, check for `.on=function`, which is a reliable way to detect `EventEmitter.on` being defined. 12 | expect(readFileSync(path.resolve(__dirname, '../dist/RosLib.umd.cjs')).includes('.on=function')).toBeFalsy(); 13 | }) 14 | }); 15 | -------------------------------------------------------------------------------- /test/examples/check-topics.example.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../../src/RosLib.js'; 3 | 4 | var expectedTopics = [ 5 | // '/turtle1/cmd_vel', '/turtle1/color_sensor', '/turtle1/pose', 6 | // '/turtle2/cmd_vel', '/turtle2/color_sensor', '/turtle2/pose', 7 | '/tf2_web_republisher/status', '/tf2_web_republisher/feedback', 8 | // '/tf2_web_republisher/goal', '/tf2_web_republisher/result', 9 | '/fibonacci/feedback', '/fibonacci/status', '/fibonacci/result' 10 | ]; 11 | 12 | describe('Example topics are live', function() { 13 | var ros = new ROSLIB.Ros({ 14 | url: 'ws://localhost:9090' 15 | }); 16 | 17 | it('getTopics', () => new Promise((done) => { 18 | ros.getTopics(function(result) { 19 | expectedTopics.forEach(function(topic) { 20 | expect(result.topics).to.contain(topic, 'Couldn\'t find topic: ' + topic); 21 | }); 22 | done(); 23 | }); 24 | })); 25 | 26 | var example = ros.Topic({ 27 | name: '/some_test_topic', 28 | messageType: 'std_msgs/String' 29 | }); 30 | 31 | it('doesn\'t automatically advertise the topic', () => new Promise((done) => { 32 | ros.getTopics(function(result) { 33 | expect(result.topics).not.to.contain('/some_test_topic'); 34 | example.advertise(); 35 | done(); 36 | }); 37 | })); 38 | 39 | it('advertise broadcasts the topic', () => new Promise((done) => { 40 | ros.getTopics(function(result) { 41 | expect(result.topics).to.contain('/some_test_topic'); 42 | example.unadvertise(); 43 | done(); 44 | }); 45 | })); 46 | 47 | it('unadvertise will end the topic (if it\s the last around)', () => new Promise((done) => { 48 | console.log('Unadvertisement test. Wait for 15 seconds..'); 49 | setTimeout(function() { 50 | ros.getTopics(function(result) { 51 | expect(result.topics).not.to.contain('/some_test_topic'); 52 | done(); 53 | }); 54 | }, 15000); 55 | }), 20000); 56 | }); 57 | -------------------------------------------------------------------------------- /test/examples/fibonacci.example.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../../src/RosLib.js'; 3 | 4 | describe('Fibonacci Example', function() { 5 | it('Fibonacci', () => new Promise((done) => { 6 | 7 | var ros = new ROSLIB.Ros({ 8 | url: 'ws://localhost:9090' 9 | }); 10 | // The ActionClient 11 | // ---------------- 12 | 13 | var fibonacciClient = new ROSLIB.ActionClient({ 14 | ros: ros, 15 | serverName: '/fibonacci', 16 | actionName: 'actionlib_tutorials/FibonacciAction' 17 | }); 18 | 19 | // Create a goal. 20 | var goal = new ROSLIB.Goal({ 21 | actionClient: fibonacciClient, 22 | goalMessage: { 23 | order: 7 24 | } 25 | }); 26 | 27 | // Print out their output into the terminal. 28 | var items = [ 29 | {'sequence': [0, 1, 1]}, 30 | {'sequence': [0, 1, 1, 2]}, 31 | {'sequence': [0, 1, 1, 2, 3]}, 32 | {'sequence': [0, 1, 1, 2, 3, 5]}, 33 | {'sequence': [0, 1, 1, 2, 3, 5, 8]}, 34 | {'sequence': [0, 1, 1, 2, 3, 5, 8, 13]}, 35 | {'sequence': [0, 1, 1, 2, 3, 5, 8, 13, 21]} 36 | ]; 37 | goal.on('feedback', function(feedback) { 38 | console.log('Feedback:', feedback); 39 | expect(feedback).to.eql(items.shift()); 40 | }); 41 | goal.on('result', function(result) { 42 | console.log('Result:', result); 43 | expect(result).to.eql({'sequence': [0, 1, 1, 2, 3, 5, 8, 13, 21]}); 44 | done(); 45 | }); 46 | 47 | // Send the goal to the action server. 48 | // The timeout is to allow rosbridge to properly subscribe all the 49 | // Action topics - otherwise, the first feedback message might get lost 50 | setTimeout(function(){ 51 | goal.send(); 52 | }, 100); 53 | }), 8000); 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /test/examples/params.examples.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../../src/RosLib.js'; 3 | 4 | describe('Param setting', function() { 5 | var ros = new ROSLIB.Ros({ 6 | url: 'ws://localhost:9090' 7 | }); 8 | var param = ros.Param({ 9 | name: '/test/foo' 10 | }); 11 | 12 | it('Param generics', function() { 13 | expect(param).to.be.instanceOf(ROSLIB.Param); 14 | expect(param.name).to.be.equal('/test/foo'); 15 | }); 16 | 17 | it('Param.set no callback', () => new Promise((done) => { 18 | param.set('foo'); 19 | setTimeout(done, 500); 20 | })); 21 | 22 | it('Param.get', () => new Promise((done) => { 23 | param.get(function(result) { 24 | expect(result).to.be.equal('foo'); 25 | done(); 26 | }); 27 | })); 28 | 29 | it('Param.set w/ callback', () => new Promise((done) => { 30 | param.set('bar', function() { 31 | done(); 32 | }); 33 | })); 34 | 35 | it('Param.get', () => new Promise((done) => { 36 | param.get(function(result) { 37 | expect(result).to.be.equal('bar'); 38 | done(); 39 | }); 40 | })); 41 | 42 | it('ros.getParams', () => new Promise((done) => { 43 | ros.getParams(function(params) { 44 | expect(params).to.include(param.name); 45 | done(); 46 | }); 47 | })); 48 | 49 | it('Param.delete', () => new Promise((done) => { 50 | param.delete(function() { 51 | ros.getParams(function(params) { 52 | expect(params).to.not.include(param.name); 53 | done(); 54 | }); 55 | }); 56 | })); 57 | }); 58 | -------------------------------------------------------------------------------- /test/examples/pubsub.example.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, afterAll } from 'vitest'; 2 | import * as ROSLIB from '../../src/RosLib.js'; 3 | 4 | describe('Topics Example', function() { 5 | 6 | var ros = new ROSLIB.Ros({ 7 | url: 'ws://localhost:9090' 8 | }); 9 | 10 | var example = ros.Topic({ 11 | name: '/example_topic', 12 | messageType: 'std_msgs/String' 13 | }); 14 | 15 | function format(msg) { 16 | return {data: msg}; 17 | } 18 | var messages1 = ['Hello Example2!', 'Whats good?'].map(format); 19 | var messages2 = ['Hi there', 'this example working'].map(format); 20 | 21 | var example2 = ros.Topic({ 22 | name: '/example_topic', 23 | messageType: 'std_msgs/String' 24 | }); 25 | 26 | it('Listening and publishing to a topic', () => new Promise((done) => { 27 | // Kind of harry... 28 | var topic1msg = messages1[0], 29 | topic2msg = {}; 30 | example.subscribe(function(message) { 31 | if (message.data === topic1msg.data) {return;} 32 | topic1msg = messages1[0]; 33 | expect(message).to.be.eql(messages2.shift()); 34 | if (messages1.length) {example.publish(topic1msg);} 35 | else {done();} 36 | }); 37 | example2.subscribe(function(message) { 38 | if (message.data === topic2msg.data) {return;} 39 | topic2msg = messages2[0]; 40 | expect(message).to.be.eql(messages1.shift()); 41 | if (messages2.length) {example2.publish(topic2msg);} 42 | else {done();} 43 | }); 44 | example.publish(topic1msg); 45 | })); 46 | 47 | it('unsubscribe doesn\'t affect other topics', () => new Promise((done) => { 48 | example2.subscribe(function(message) { 49 | // should never be called 50 | expect(false).to.be.ok; 51 | }); 52 | example.unsubscribe(); 53 | example2.removeAllListeners('message'); 54 | example2.subscribe(function(message) { 55 | expect(message).to.be.eql({ 56 | data: 'hi' 57 | }); 58 | done(); 59 | }); 60 | example.publish({ 61 | data: 'hi' 62 | }); 63 | })); 64 | 65 | it('unadvertise doesn\'t affect other topics', () => new Promise((done) => { 66 | example.unsubscribe(); 67 | example2.unadvertise(); 68 | example2.removeAllListeners('message'); 69 | example2.subscribe(function(message) { 70 | expect(example2.isAdvertised).to.be.false; 71 | expect(message).to.be.eql({ 72 | data: 'hi' 73 | }); 74 | done(); 75 | }); 76 | example.publish({ 77 | data: 'hi' 78 | }); 79 | })); 80 | 81 | it('unsubscribing from all Topics should stop the socket from receiving data (on that topic', () => new Promise((done) => { 82 | example.unsubscribe(); 83 | example2.unsubscribe(); 84 | ros.on('/example_topic', function() { 85 | expect(false).to.be.ok; 86 | }); 87 | example.publish({ 88 | data: 'sup' 89 | }); 90 | setTimeout(done, 500); 91 | })); 92 | 93 | afterAll(function() { 94 | example.unadvertise(); 95 | example.unsubscribe(); 96 | example2.unadvertise(); 97 | example2.unsubscribe(); 98 | }); 99 | }, 1000); 100 | 101 | -------------------------------------------------------------------------------- /test/examples/setup_examples.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | if command -v rosrun 2>/dev/null 4 | then 5 | echo "Shutting everything down" 6 | pgrep -f "[r]os" | xargs kill -9 7 | sleep 1 8 | 9 | echo "Starting roscore and various examples in background processes" 10 | roslaunch examples/setup_examples.launch > roslaunch.log & 11 | 12 | LAUNCHED=false 13 | for i in {1..10} 14 | do 15 | echo "Waiting for /hello_world_publisher...$i" 16 | sleep 1 17 | rostopic info /listener > /dev/null && LAUNCHED=true && break 18 | done 19 | if [ $LAUNCHED == true ] 20 | then 21 | echo "Ready for lift off" 22 | exit 0 23 | else 24 | echo "/hello_world_publisher not launched" 25 | exit 1 26 | fi 27 | else 28 | echo "Couldn't find ROS on path (try to source it)" 29 | # shellcheck disable=SC2016 30 | echo 'source /opt/ros/$ROS_DISTRO/setup.bash' 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /test/examples/setup_examples.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/examples/tf.example.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../../src/RosLib.js'; 3 | 4 | describe('TF2 Republisher Example', function() { 5 | it('tf republisher', () => new Promise((done) => { 6 | var ros = new ROSLIB.Ros(); 7 | ros.connect('ws://localhost:9090'); 8 | 9 | var tfClient = new ROSLIB.TFClient({ 10 | ros: ros, 11 | fixedFrame: 'world', 12 | angularThres: 0.01, 13 | transThres: 0.01 14 | }); 15 | 16 | // Subscribe to a turtle. 17 | tfClient.subscribe('turtle1', function(tf) { 18 | expect(tf.rotation).to.be.eql(new ROSLIB.Quaternion()); 19 | expect(tf.translation).to.be.a.instanceof(ROSLIB.Vector3); 20 | done(); 21 | }); 22 | })); 23 | 24 | it('tf republisher alternative syntax', () => new Promise((done) => { 25 | var ros = new ROSLIB.Ros({ 26 | url: 'ws://localhost:9090' 27 | }); 28 | 29 | var tfClient = ros.TFClient({ 30 | fixedFrame: 'world', 31 | angularThres: 0.01, 32 | transThres: 0.01 33 | }); 34 | 35 | // Subscribe to a turtle. 36 | tfClient.subscribe('turtle1', function(tf) { 37 | expect(tf.rotation).to.be.eql(new ROSLIB.Quaternion()); 38 | expect(tf.translation).to.be.a.instanceof(ROSLIB.Vector3); 39 | done(); 40 | }); 41 | })); 42 | }); 43 | -------------------------------------------------------------------------------- /test/examples/tf_service.example.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../../src/RosLib.js'; 3 | 4 | describe('TF2 Republisher Service Example', function() { 5 | it('tf republisher', () => new Promise((done) => { 6 | var ros = new ROSLIB.Ros({ 7 | // Use the service interface to tf2_web_republisher 8 | groovyCompatibility: false 9 | }); 10 | ros.connect('ws://localhost:9090'); 11 | 12 | var tfClient = new ROSLIB.TFClient({ 13 | ros: ros, 14 | fixedFrame: 'world', 15 | angularThres: 0.01, 16 | transThres: 0.01 17 | }); 18 | 19 | // Subscribe to a turtle. 20 | tfClient.subscribe('turtle1', function(tf) { 21 | expect(tf.rotation).to.be.eql(new ROSLIB.Quaternion()); 22 | expect(tf.translation).to.be.a.instanceof(ROSLIB.Vector3); 23 | done(); 24 | }); 25 | })); 26 | 27 | it('tf republisher alternative syntax', () => new Promise((done) => { 28 | var ros = new ROSLIB.Ros({ 29 | url: 'ws://localhost:9090' 30 | }); 31 | 32 | var tfClient = ros.TFClient({ 33 | fixedFrame: 'world', 34 | angularThres: 0.01, 35 | transThres: 0.01 36 | }); 37 | 38 | // Subscribe to a turtle. 39 | tfClient.subscribe('turtle1', function(tf) { 40 | expect(tf.rotation).to.be.eql(new ROSLIB.Quaternion()); 41 | expect(tf.translation).to.be.a.instanceof(ROSLIB.Vector3); 42 | done(); 43 | }); 44 | })); 45 | }); 46 | -------------------------------------------------------------------------------- /test/examples/topic-listener.example.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../../src/RosLib.js'; 3 | 4 | var ros = new ROSLIB.Ros({ 5 | url: 'ws://localhost:9090' 6 | }); 7 | 8 | function format(msg) { 9 | return {data: msg}; 10 | } 11 | var messages = ['1', '2', '3', '4'].map(format); 12 | 13 | describe('Topics Example', function() { 14 | 15 | function createAndStreamTopic(topicName) { 16 | var topic = ros.Topic({ 17 | name: topicName, 18 | messageType: 'std_msgs/String' 19 | }); 20 | var idx = 0; 21 | 22 | function emit() { 23 | setTimeout(function() { 24 | topic.publish(messages[idx++]); 25 | if (idx < messages.length) { 26 | emit(); 27 | } else { 28 | topic.unsubscribe(); 29 | topic.unadvertise(); 30 | } 31 | }, 50); 32 | } 33 | emit(); 34 | 35 | return topic; 36 | } 37 | 38 | 39 | it('Listening to a topic & unsubscribes', () => new Promise((done) => { 40 | var topic = createAndStreamTopic('/echo/test'); 41 | var expected = messages.slice(); 42 | 43 | topic.subscribe(function(message) { 44 | expect(message).to.be.eql(expected.shift()); 45 | }); 46 | 47 | topic.on('unsubscribe', done); 48 | })); 49 | }, 1000); 50 | -------------------------------------------------------------------------------- /test/math-examples.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../src/RosLib.js'; 3 | 4 | function clone(x) { 5 | var y = {}; 6 | for (var prop in x) { 7 | if (x.hasOwnProperty(prop)) { 8 | y[prop] = typeof x[prop] === 'object' ? clone(x[prop]) : x[prop]; 9 | } 10 | } 11 | return y; 12 | } 13 | 14 | describe('Math examples', function() { 15 | var v1, q1, v2, q2; 16 | var pos; 17 | it('Vector3 example', function() { 18 | // Let's start by adding some vectors. 19 | v1 = new ROSLIB.Vector3({ 20 | x: 1, 21 | y: 2, 22 | z: 3 23 | }); 24 | v2 = v1.clone(); 25 | expect(v1).not.equal(v2); 26 | expect(v1).eql(v2); 27 | 28 | v1.add(v2); 29 | expect(clone(v1)).eql({ 30 | x: 2, 31 | y: 4, 32 | z: 6 33 | }); 34 | }); 35 | 36 | it('Quaternion example', function() { 37 | // Now let's play with some quaternions. 38 | q1 = new ROSLIB.Quaternion({ 39 | x: 0.1, 40 | y: 0.2, 41 | z: 0.3, 42 | w: 0.4 43 | }); 44 | q2 = q1.clone(); 45 | expect(q1).not.equal(q2); 46 | expect(q1).eql(q2); 47 | 48 | q1.multiply(q2); 49 | q1.invert(); 50 | expect(q1.x).to.be.within(-0.26667, -0.26666); 51 | expect(q1.y).to.be.within(-0.53334, -0.53333); 52 | expect(q1.z).to.be.within(-0.80000, -0.79999); 53 | expect(q1.w).to.be.within(0.06666, 0.06667); 54 | }); 55 | 56 | it('Pose example', function() { 57 | // Let's copy the results into a pose. 58 | pos = new ROSLIB.Pose({ 59 | position: v1, 60 | orientation: q1 61 | }); 62 | expect(clone(pos)).to.eql(clone({position: v1, orientation: q1})); 63 | }); 64 | 65 | it('Transform example', function() { 66 | // Finally, let's play with some transforms. 67 | var tf = new ROSLIB.Transform({ 68 | translation: v2, 69 | rotation: q2 70 | }); 71 | pos.applyTransform(tf); 72 | expect(pos.orientation.x).to.be.within(-0.1, -0.09999); 73 | expect(pos.orientation.y).to.be.within(-0.20001, -0.20000); 74 | expect(pos.orientation.z).to.be.within(-0.3, -0.3); 75 | expect(pos.orientation.w).to.be.within(0.39999, 0.4); 76 | 77 | expect(pos.position.x).to.be.within(1.6, 1.60001); 78 | expect(pos.position.y).to.be.within(3.2, 3.20001); 79 | expect(pos.position.z).to.be.within(4.8, 4.80001); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/quaternion.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../src/RosLib.js'; 3 | 4 | 5 | describe('Quaternion', function() { 6 | 7 | describe('creation', function() { 8 | // Test fails. Claims returning Object. 9 | // it('should return an object of the correct type', function() { 10 | // var q = new ROSLIB.Quaternion(); 11 | // expect(q).to.be.a('ROSLIB.Quaternion'); 12 | // }); 13 | it('should return an identity quaternion when no params are specified', function() { 14 | var q = new ROSLIB.Quaternion(); 15 | expect(q.x).to.equal(0); 16 | expect(q.y).to.equal(0); 17 | expect(q.z).to.equal(0); 18 | expect(q.w).to.equal(1); 19 | }); 20 | it('should return an identity quaternion when null is specified', function() { 21 | var q = new ROSLIB.Quaternion({ x: null, y: null, z: null, w: null }); 22 | expect(q.x).to.equal(0); 23 | expect(q.y).to.equal(0); 24 | expect(q.z).to.equal(0); 25 | expect(q.w).to.equal(1); 26 | }); 27 | it('should return a quaternion matching the options hash', function() { 28 | var q = new ROSLIB.Quaternion({ x: 1.1, y: 2.2, z: 3.3, w: 4.4 }); 29 | expect(q.x).to.equal(1.1); 30 | expect(q.y).to.equal(2.2); 31 | expect(q.z).to.equal(3.3); 32 | expect(q.w).to.equal(4.4); 33 | }); 34 | it('should return a quaternion matching the options', function() { 35 | var q = new ROSLIB.Quaternion({ x: 1, y: 0, z: 0, w: 0 }); 36 | expect(q.x).to.equal(1); 37 | expect(q.y).to.equal(0); 38 | expect(q.z).to.equal(0); 39 | expect(q.w).to.equal(0); 40 | 41 | q = new ROSLIB.Quaternion({ x: 0, y: 1, z: 0, w: 0 }); 42 | expect(q.x).to.equal(0); 43 | expect(q.y).to.equal(1); 44 | expect(q.z).to.equal(0); 45 | expect(q.w).to.equal(0); 46 | 47 | q = new ROSLIB.Quaternion({ x: 0, y: 0, z: 1, w: 0 }); 48 | expect(q.x).to.equal(0); 49 | expect(q.y).to.equal(0); 50 | expect(q.z).to.equal(1); 51 | expect(q.w).to.equal(0); 52 | }); 53 | }); 54 | 55 | describe('conjugation', function() { 56 | it('should conjugate itself', function() { 57 | var q = new ROSLIB.Quaternion({ x: 1.1, y: 2.2, z: 3.3, w: 4.4 }); 58 | q.conjugate(); 59 | expect(q.x).to.equal(1.1*-1); 60 | expect(q.y).to.equal(2.2*-1); 61 | expect(q.z).to.equal(3.3*-1); 62 | }); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/service.test.js: -------------------------------------------------------------------------------- 1 | import { it, describe, expect } from 'vitest'; 2 | import { Service, Ros } from '../'; 3 | 4 | describe('Service', () => { 5 | const ros = new Ros({ 6 | url: 'ws://localhost:9090' 7 | }); 8 | it('Successfully advertises a service with an async return', async () => { 9 | const server = new Service({ 10 | ros, 11 | serviceType: 'std_srvs/Trigger', 12 | name: '/test_service' 13 | }); 14 | server.advertiseAsync(async () => { 15 | return { 16 | success: true, 17 | message: 'foo' 18 | } 19 | }); 20 | const client = new Service({ 21 | ros, 22 | serviceType: 'std_srvs/Trigger', 23 | name: '/test_service' 24 | }) 25 | const response = await new Promise((resolve, reject) => client.callService({}, resolve, reject)); 26 | expect(response).toEqual({success: true, message: 'foo'}); 27 | // Make sure un-advertisement actually disposes of the event handler 28 | expect(ros.listenerCount(server.name)).toEqual(1); 29 | server.unadvertise(); 30 | expect(ros.listenerCount(server.name)).toEqual(0); 31 | }) 32 | it('Successfully advertises a service with a synchronous return', async () => { 33 | const server = new Service({ 34 | ros, 35 | serviceType: 'std_srvs/Trigger', 36 | name: '/test_service' 37 | }); 38 | server.advertise((request, response) => { 39 | response.success = true; 40 | response.message = 'bar'; 41 | return true; 42 | }); 43 | const client = new Service({ 44 | ros, 45 | serviceType: 'std_srvs/Trigger', 46 | name: '/test_service' 47 | }) 48 | const response = await new Promise((resolve, reject) => client.callService({}, resolve, reject)); 49 | expect(response).toEqual({success: true, message: 'bar'}); 50 | // Make sure un-advertisement actually disposes of the event handler 51 | expect(ros.listenerCount(server.name)).toEqual(1); 52 | server.unadvertise(); 53 | expect(ros.listenerCount(server.name)).toEqual(0); 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/tfclient.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../src/RosLib.js'; 3 | 4 | describe('TFClient', function() { 5 | 6 | describe('dispose', function() { 7 | 8 | it('should not subscribe to republished topic if already disposed', function() { 9 | // This test makes sure we do not subscribe to the republished topic if the 10 | // tf client has already been disposed when we get the response (from the setup request) 11 | // from the server. 12 | 13 | var dummyROS = { 14 | idCounter: 0, 15 | on: () => {}, 16 | off: () => {}, 17 | callOnConnection: () => {} 18 | }; 19 | 20 | // @ts-expect-error -- stub impl 21 | var tfclient = new ROSLIB.TFClient({ ros: dummyROS }); 22 | tfclient.dispose(); 23 | 24 | // Simulated a response from the server after the client is already disposed 25 | tfclient.processResponse({ topic_name: '/repub_1' }); 26 | 27 | expect(tfclient.currentTopic).to.be.false; 28 | }); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /test/transform.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../src/RosLib.js'; 3 | 4 | describe('Transform', function() { 5 | 6 | describe('creation', function() { 7 | // Fails test. Claims type is Object. 8 | // it('should return an object of the correct type', function() { 9 | // var t = new ROSLIB.Transform(); 10 | // expect(t).to.be.a('ROSLIB.Transform'); 11 | // }); 12 | it('should contain a valid vector and quaternion', function() { 13 | var t = new ROSLIB.Transform({ 14 | translation: new ROSLIB.Vector3({ x: 1, y: 2, z: 3 }), 15 | rotation: new ROSLIB.Quaternion({ x: 0.9, y: 0.8, z: 0.7, w: 1 }) 16 | }); 17 | // expect(t.translation).to.be.a('ROSLIB.Vector3'); 18 | expect(t.translation.x).to.equal(1); 19 | // expect(t.rotation).to.be.a('ROSLIB.Quaternion'); 20 | expect(t.rotation.z).to.equal(0.7); 21 | expect(t.rotation.w).to.equal(1); 22 | }); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/urdf.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as ROSLIB from '../src/RosLib.js'; 3 | 4 | import { DOMParser } from '@xmldom/xmldom'; 5 | // See https://developer.mozilla.org/docs/XPathResult#Constants 6 | var XPATH_FIRST_ORDERED_NODE_TYPE = 9; 7 | 8 | var sample_urdf = function (){ 9 | return '' + 10 | ' '+ // test well-behaved versions of the basic shapes 11 | ' '+ 12 | ' '+ 13 | ' '+ 14 | ' '+ 15 | ' '+ 16 | ' '+ 17 | ' '+ 18 | ' '+ 19 | ' '+ 20 | ' '+ 21 | ' '+ 22 | ' '+ 23 | ' '+ 24 | ' '+ 25 | ' '+ 26 | ' '+ 27 | ' '+ 28 | ' '+ 29 | ' '+ 30 | ' '+ 31 | ' '+ // and an extra one with a material 32 | ' '+ 33 | ' '+ 34 | ' '+ 35 | ' '+ 36 | ' '+ 37 | ' '+ 38 | ' '+ 39 | ' '+ 40 | ' '+ 41 | ' '+ // link with referenced material and multiple visuals 42 | ' '+ 43 | ' '+ 44 | ' '+ 45 | ' '+ 46 | ' '+ 47 | ' '+ 48 | ' '+ 49 | ' '+ 50 | ' '+ 51 | ' '+ 52 | ' '+ 53 | ' '+ 54 | ' '+ 55 | ' '+ 56 | ' '+ 57 | ' '+ 58 | ' '+ 59 | ' '+ 60 | ' '+ 61 | ' '+ 62 | ' '+ 63 | ' '+ 64 | ' '+ 65 | ' '+ 66 | ' '+ 67 | ' '+ 68 | ' '+ 69 | ' '+ 70 | ''; 71 | } 72 | 73 | describe('URDF', function() { 74 | 75 | describe('parsing', function() { 76 | it('should load simple xml', function() { 77 | // http://wiki.ros.org/urdf/Tutorials/Create%20your%20own%20urdf%20file 78 | var urdfModel = new ROSLIB.UrdfModel({ 79 | string: sample_urdf() 80 | }); 81 | 82 | expect(urdfModel.name).to.equal('test_robot'); 83 | }); 84 | 85 | it('should correctly construct visual elements', function() { 86 | var urdfModel = new ROSLIB.UrdfModel({ 87 | string: sample_urdf() 88 | }); 89 | 90 | // Check all the visual elements 91 | expect(urdfModel.links['link1'].visuals.length).to.equal(1); 92 | expect(urdfModel.links['link1'].visuals[0].geometry.radius).to.equal(1.0); 93 | expect(urdfModel.links['link2'].visuals[0].geometry.dimension.x).to.equal(0.5); 94 | expect(urdfModel.links['link2'].visuals[0].geometry.dimension.y).to.equal(0.5); 95 | expect(urdfModel.links['link2'].visuals[0].geometry.dimension.z).to.equal(0.5); 96 | expect(urdfModel.links['link3'].visuals[0].geometry.length).to.equal(2.0); 97 | expect(urdfModel.links['link3'].visuals[0].geometry.radius).to.equal(0.2); 98 | 99 | expect(urdfModel.links['link4'].visuals.length).to.equal(1); 100 | expect(urdfModel.links['link4'].visuals[0].material.name).to.equal('red'); 101 | expect(urdfModel.links['link4'].visuals[0].material.color.r).to.equal(1.0); 102 | expect(urdfModel.links['link4'].visuals[0].material.color.g).to.equal(0); 103 | expect(urdfModel.links['link4'].visuals[0].material.color.b).to.equal(0); 104 | expect(urdfModel.links['link4'].visuals[0].material.color.a).to.equal(1.0); 105 | 106 | expect(urdfModel.links['link5'].visuals.length).to.equal(2); 107 | expect(urdfModel.links['link5'].visuals[0].material.name).to.equal('blue'); 108 | expect(urdfModel.links['link5'].visuals[0].material.color.r).to.equal(0.0); 109 | expect(urdfModel.links['link5'].visuals[0].material.color.g).to.equal(0.0); 110 | expect(urdfModel.links['link5'].visuals[0].material.color.b).to.equal(1.0); 111 | expect(urdfModel.links['link5'].visuals[0].material.color.a).to.equal(1.0); 112 | }); 113 | 114 | it('is ignorant to the xml node', function(){ 115 | var parser = new DOMParser(); 116 | var xml = parser.parseFromString(sample_urdf(), 'text/xml'); 117 | var robotXml = xml.documentElement; 118 | expect(robotXml.getAttribute('name')).to.equal('test_robot'); 119 | }); 120 | }); 121 | 122 | }); 123 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "module": "esnext" /* Specify what module code is generated. */, 5 | "rootDir": "./src" /* Specify the root folder within your source files. */, 6 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 7 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 8 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 9 | "outDir": "./tsbuild" /* Specify an output folder for all emitted files. */, 10 | "declarationDir": "./build" /* Specify the output directory for generated declaration files. */, 11 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 12 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 13 | "strict": true /* Enable all strict type-checking options. */, 14 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 15 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 16 | "moduleResolution": "bundler", 17 | "types": ["@types/node"] 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import dts from 'vite-plugin-dts'; 4 | import checker from 'vite-plugin-checker'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | dts({ 9 | insertTypesEntry: true 10 | }), 11 | checker({ 12 | typescript: true, 13 | eslint: { 14 | lintCommand: 'eslint .', 15 | useFlatConfig: true 16 | } 17 | }) 18 | ], 19 | build: { 20 | lib: { 21 | // Could also be a dictionary or array of multiple entry points 22 | entry: resolve(__dirname, 'src/RosLib.js'), 23 | name: 'ROSLIB', 24 | // the proper extensions will be added 25 | fileName: 'RosLib', 26 | }, 27 | rollupOptions: { 28 | // make sure to externalize deps that shouldn't be bundled 29 | // into your library 30 | external: ['eventemitter3', 'ws', 'src/util/decompressPng.js'], 31 | output: { 32 | globals: { eventemitter3: 'EventEmitter3' } 33 | } 34 | }, 35 | }, 36 | test: { 37 | include: [ 38 | '{src,test}/**\/*.{test,spec}.?(c|m)[jt]s?(x)', 39 | './test/examples/*.js', 40 | ], 41 | exclude: ['dist'], 42 | environmentMatchGlobs: [ 43 | // React example requires DOM emulation 44 | ['examples/react-example/**', 'jsdom'] 45 | ] 46 | } 47 | }) 48 | --------------------------------------------------------------------------------