├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── stale.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── extensions.json └── settings.json ├── AUTHORS.md ├── LICENSE ├── QUALITY_DECLARATION.md ├── README.md ├── ROSBRIDGE_PROTOCOL.md ├── TROUBLESHOOTING.md ├── pyproject.toml ├── rosapi ├── CHANGELOG.rst ├── CMakeLists.txt ├── README.md ├── mapping_rules.yaml ├── package.xml ├── scripts │ └── rosapi_node ├── src │ └── rosapi │ │ ├── __init__.py │ │ ├── async_helper.py │ │ ├── glob_helper.py │ │ ├── objectutils.py │ │ ├── params.py │ │ ├── proxy.py │ │ └── stringify_field_types.py └── test │ ├── __init__.py │ ├── test_stringify_field_types.py │ └── test_typedefs.py ├── rosapi_msgs ├── CHANGELOG.rst ├── CMakeLists.txt ├── msg │ └── TypeDef.msg ├── package.xml └── srv │ ├── ActionFeedbackDetails.srv │ ├── ActionGoalDetails.srv │ ├── ActionResultDetails.srv │ ├── DeleteParam.srv │ ├── GetActionServers.srv │ ├── GetParam.srv │ ├── GetParamNames.srv │ ├── GetROSVersion.srv │ ├── GetTime.srv │ ├── HasParam.srv │ ├── Interfaces.srv │ ├── MessageDetails.srv │ ├── NodeDetails.srv │ ├── Nodes.srv │ ├── Publishers.srv │ ├── ServiceNode.srv │ ├── ServiceProviders.srv │ ├── ServiceRequestDetails.srv │ ├── ServiceResponseDetails.srv │ ├── ServiceType.srv │ ├── Services.srv │ ├── ServicesForType.srv │ ├── SetParam.srv │ ├── Subscribers.srv │ ├── TopicType.srv │ ├── Topics.srv │ ├── TopicsAndRawTypes.srv │ └── TopicsForType.srv ├── rosbridge_library ├── CHANGELOG.rst ├── CMakeLists.txt ├── package.xml ├── src │ └── rosbridge_library │ │ ├── __init__.py │ │ ├── capabilities │ │ ├── __init__.py │ │ ├── action_feedback.py │ │ ├── action_result.py │ │ ├── advertise.py │ │ ├── advertise_action.py │ │ ├── advertise_service.py │ │ ├── call_service.py │ │ ├── defragmentation.py │ │ ├── fragmentation.py │ │ ├── publish.py │ │ ├── send_action_goal.py │ │ ├── service_response.py │ │ ├── subscribe.py │ │ ├── unadvertise_action.py │ │ └── unadvertise_service.py │ │ ├── capability.py │ │ ├── internal │ │ ├── __init__.py │ │ ├── actions.py │ │ ├── cbor_conversion.py │ │ ├── exceptions.py │ │ ├── message_conversion.py │ │ ├── outgoing_message.py │ │ ├── pngcompression.py │ │ ├── publishers.py │ │ ├── ros_loader.py │ │ ├── services.py │ │ ├── subscribers.py │ │ ├── subscription_modifiers.py │ │ └── topics.py │ │ ├── protocol.py │ │ ├── rosbridge_protocol.py │ │ └── util │ │ ├── __init__.py │ │ ├── cbor.py │ │ └── ros.py └── test │ ├── __init__.py │ ├── capabilities │ ├── __init__.py │ ├── test_action_capabilities.py │ ├── test_advertise.py │ ├── test_call_service.py │ ├── test_publish.py │ ├── test_service_capabilities.py │ └── test_subscribe.py │ ├── experimental │ ├── complex_srv+tcp │ │ ├── test_non-ros_service_client_complex-srv.py │ │ └── test_non-ros_service_server_complex-srv.py │ └── fragmentation+srv+tcp │ │ ├── test_non-ros_service_client_fragmented.py │ │ └── test_non-ros_service_server_fragmented.py │ ├── internal │ ├── __init__.py │ ├── actions │ │ └── test_actions.py │ ├── publishers │ │ ├── __init__.py │ │ ├── test_multi_publisher.py │ │ └── test_publisher_manager.py │ ├── services │ │ └── test_services.py │ ├── subscribers │ │ ├── __init__.py │ │ ├── test_multi_subscriber.py │ │ ├── test_subscriber_manager.py │ │ └── test_subscription_modifiers.py │ ├── test_cbor_conversion.py │ ├── test_compression.py │ ├── test_message_conversion.py │ ├── test_outgoing_message.py │ └── test_ros_loader.py │ └── test_all.test ├── rosbridge_msgs ├── CHANGELOG.rst ├── CMakeLists.txt ├── msg │ ├── ConnectedClient.msg │ └── ConnectedClients.msg └── package.xml ├── rosbridge_server ├── CHANGELOG.rst ├── CMakeLists.txt ├── launch │ └── rosbridge_websocket_launch.xml ├── package.xml ├── scripts │ ├── rosbridge_websocket │ └── rosbridge_websocket.py ├── src │ └── rosbridge_server │ │ ├── __init__.py │ │ ├── client_manager.py │ │ └── websocket_handler.py └── test │ └── websocket │ ├── advertise_action.test.py │ ├── advertise_action_feedback.test.py │ ├── advertise_service.test.py │ ├── advertise_service_duplicate.test.py │ ├── best_effort_publisher.test.py │ ├── call_service.test.py │ ├── common.py │ ├── multiple_subscribers.test.py │ ├── multiple_subscribers_raw.test.py │ ├── send_action_goal.test.py │ ├── smoke.test.py │ └── transient_local_publisher.test.py ├── rosbridge_suite ├── CHANGELOG.rst ├── CMakeLists.txt └── package.xml ├── rosbridge_test_msgs ├── CHANGELOG.rst ├── CMakeLists.txt ├── action │ ├── TestEmpty.action │ ├── TestFeedbackAndResult.action │ ├── TestGoalAndResult.action │ ├── TestGoalFeedbackAndResult.action │ ├── TestGoalOnly.action │ ├── TestMultipleGoalFields.action │ └── TestResultOnly.action ├── msg │ ├── Num.msg │ ├── TestChar.msg │ ├── TestDurationArray.msg │ ├── TestFloat32Array.msg │ ├── TestFloat32BoundedArray.msg │ ├── TestHeader.msg │ ├── TestHeaderArray.msg │ ├── TestHeaderTwo.msg │ ├── TestNestedBoundedArray.msg │ ├── TestTimeArray.msg │ ├── TestUInt8.msg │ └── TestUInt8FixedSizeArray16.msg ├── package.xml └── srv │ ├── AddTwoInts.srv │ ├── SendBytes.srv │ ├── TestArrayRequest.srv │ ├── TestEmpty.srv │ ├── TestMultipleRequestFields.srv │ ├── TestMultipleResponseFields.srv │ ├── TestNestedService.srv │ ├── TestRequestAndResponse.srv │ ├── TestRequestOnly.srv │ └── TestResponseOnly.srv └── setup.cfg /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ## Expected Behavior 6 | 7 | 8 | ## Actual Behavior 9 | 10 | 11 | ## Steps to Reproduce the Problem 12 | 13 | 1. 14 | 1. 15 | 1. 16 | 17 | ## Specifications 18 | 19 | - ROS Version (`echo $ROS_DISTRO`): 20 | - OS Version (`grep DISTRIB_CODENAME /etc/lsb-release`): 21 | - Rosbridge Version (`roscat rosbridge_server package.xml | grep ''`): 22 | - Tornado Version (`python -c 'import tornado; print tornado.version'`): 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - uses: actions/setup-python@v5 12 | with: 13 | python-version: '3.10' 14 | 15 | - uses: pre-commit/action@v3.0.1 16 | 17 | test: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | # Test supported ROS 2 distributions 23 | # https://docs.ros.org/en/rolling/Releases.html 24 | # NOTE: Humble does not work on the `ros2` branch, so it is tested in its own branch. 25 | - ros: jazzy 26 | os: ubuntu-24.04 27 | - ros: kilted 28 | os: ubuntu-24.04 29 | - ros: rolling 30 | os: ubuntu-24.04 31 | 32 | name: ROS 2 ${{ matrix.ros }} (${{ matrix.os }}) 33 | runs-on: ${{ matrix.os }} 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | path: ros_ws/src 39 | 40 | - uses: ros-tooling/setup-ros@v0.7 41 | 42 | - uses: ros-tooling/action-ros-ci@v0.4 43 | with: 44 | target-ros2-distro: ${{ matrix.ros }} 45 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@v9.1.0 15 | with: 16 | stale-issue-message: "This issue has been marked as stale because there has been no activity in the past 12 months. Please add a comment to keep it open." 17 | stale-issue-label: stale 18 | days-before-issue-stale: 365 19 | days-before-issue-close: 30 20 | 21 | stale-pr-message: "This PR has been marked as stale because there has been no activity in the past 6 months. Please add a comment to keep it open." 22 | stale-pr-label: stale 23 | days-before-pr-stale: 180 24 | days-before-pr-close: 30 25 | delete-branch: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.iml 3 | *.swp 4 | .project 5 | .pydevproject 6 | .DS_Store 7 | .idea 8 | 9 | .vscode/* 10 | !.vscode/extensions.json 11 | !.vscode/settings.json 12 | 13 | # colcon/CMake output 14 | build 15 | install 16 | log 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.6.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-case-conflict 8 | - id: check-merge-conflict 9 | - id: check-symlinks 10 | - id: check-yaml 11 | - id: debug-statements 12 | - id: destroyed-symlinks 13 | - id: detect-private-key 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | - id: trailing-whitespace 17 | 18 | - repo: https://github.com/pycqa/isort 19 | rev: 5.13.2 20 | hooks: 21 | - id: isort 22 | 23 | - repo: https://github.com/psf/black 24 | rev: 24.8.0 25 | hooks: 26 | - id: black 27 | 28 | - repo: https://github.com/PyCQA/flake8 29 | rev: 7.1.1 30 | hooks: 31 | - id: flake8 32 | 33 | - repo: https://github.com/PyCQA/bandit 34 | rev: 1.7.10 35 | hooks: 36 | - id: bandit 37 | args: ["--skip", "B101,B110,B311"] 38 | 39 | - repo: https://github.com/codespell-project/codespell 40 | rev: v2.3.0 41 | hooks: 42 | - id: codespell 43 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-iot.vscode-ros", 5 | "bungcip.better-toml" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "python.linting.enabled": true, 4 | "python.linting.flake8Enabled": true, 5 | "python.linting.banditEnabled": true, 6 | "python.linting.pylintEnabled": false, 7 | "[python]": { 8 | "editor.formatOnSave": true, 9 | "editor.codeActionsOnSave": { 10 | "source.organizeImports": true 11 | } 12 | }, 13 | "python.linting.banditArgs": [ 14 | "--skip", 15 | "B101,B110,B311" 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ## Contributors 2 | 3 | - Brandon Alexander 4 | - David Bertram 5 | - Hans-Joachim Krauch 6 | - Jacob Bandes-Storch 7 | - Jihoon Lee 8 | - Jonathan Mace 9 | - Matthias Gruhler 10 | - Russell Toris 11 | - Travis Prosser 12 | - Błażej Sowa 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) The Rosbridge Authors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /QUALITY_DECLARATION.md: -------------------------------------------------------------------------------- 1 | This document is a declaration of software quality for the `rosbridge_suite` packages, based on the guidelines in [REP-2004](https://www.ros.org/reps/rep-2004.html). The following packages are covered by this declaration: 2 | 3 | - rosapi 4 | - rosapi_msgs 5 | - rosbridge_library 6 | - rosbridge_msgs 7 | - rosbridge_server 8 | - rosbridge_suite 9 | - rosbridge_test_msgs 10 | 11 | # `rosbridge_suite` Quality Declaration 12 | 13 | The package `rosbridge_suite` claims to be in the **Quality Level 3** category. 14 | 15 | Below are the rationales, notes, and caveats for this claim, organized by each requirement listed in the [Package Requirements for Quality Level N in REP-2004](https://www.ros.org/reps/rep-2004.html). 16 | 17 | ## Version Policy [1] 18 | 19 | ### Version Scheme [1.i] 20 | 21 | `rosbridge_suite` uses [semver](https://semver.org/) according to the recommendation in the [ROS 2 Developer Guide](https://docs.ros.org/en/rolling/Contributing/Developer-Guide.html#versioning). 22 | 23 | ### Version Stability [1.ii] 24 | 25 | `rosbridge_suite` is at a stable version, i.e. `>= 1.0.0`. The current version can be found in its [package.xml](/rosbridge_server/package.xml), and its change history can be found in its [CHANGELOG](/rosbridge_server/CHANGELOG.rst). 26 | 27 | ### Public API Declaration [1.iii] 28 | 29 | The public API is not explicitly defined. 30 | 31 | ### API Stability Within a Released ROS Distribution [1.iv]/[1.vi] 32 | 33 | The public API is not guaranteed to be stable within a released ROS distribution. 34 | 35 | ### ABI Stability Within a Released ROS Distribution [1.v]/[1.vi] 36 | 37 | `rosbridge_suite` is written in Python and therefore is not concerned with ABI stability. 38 | 39 | ## Change Control Process [2] 40 | 41 | `rosbridge_suite` follows the recommended guidelines for ROS Core packages in the [ROS 2 Developer Guide](https://docs.ros.org/en/rolling/Contributing/Developer-Guide.html#change-control-process). 42 | 43 | ### Change Requests [2.i] 44 | 45 | All changes will occur through a pull request, check [ROS 2 Developer Guide](https://index.ros.org/doc/ros2/Contributing/Developer-Guide/#change-control-process) for additional information. 46 | 47 | ### Continuous Integration [2.ii] 48 | 49 | All pull request must pass CI on all supported platforms. 50 | 51 | ## Documentation [3] 52 | 53 | ### License [3.i] 54 | 55 | The license for `rosbridge_suite` is BSD 3-clause, and a full copy is in the [LICENSE](/LICENSE) file. 56 | 57 | ### Copyright Statement [3.ii] 58 | 59 | The copyright statement is in the [LICENSE](/LICENSE) file. 60 | 61 | ## Testing [4] 62 | 63 | New features are required to have tests before being added. 64 | 65 | ## Dependencies [5] 66 | 67 | 68 | `rosbridge_suite` has a runtime dependency on `rclpy`, which does not have a quality declaration. 69 | 70 | ## Platform Support [6] 71 | 72 | `rosbridge_suite` supports all of the tier 1 platforms as described in [REP-2000](https://www.ros.org/reps/rep-2000.html#support-tiers). Tests are currently only run against Ubuntu Linux. 73 | 74 | ## Security [7] 75 | 76 | ### Vulnerability Disclosure Policy [7.i] 77 | 78 | This package conforms to the Vulnerability Disclosure Policy in [REP-2006](https://www.ros.org/reps/rep-2006.html). 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rosbridge_suite 2 | =============== 3 | 4 | [![ROS Humble version](https://img.shields.io/ros/v/humble/rosbridge_suite)](https://index.ros.org/p/rosbridge_suite/github-RobotWebTools-rosbridge_suite/#humble) 5 | [![ROS Jazzy version](https://img.shields.io/ros/v/jazzy/rosbridge_suite)](https://index.ros.org/p/rosbridge_suite/github-RobotWebTools-rosbridge_suite/#jazzy) 6 | [![ROS Rolling version](https://img.shields.io/ros/v/rolling/rosbridge_suite)](https://index.ros.org/p/rosbridge_suite/github-RobotWebTools-rosbridge_suite/#rolling) 7 | 8 | 9 | #### Server Implementations of the rosbridge v2 Protocol 10 | 11 | rosbridge provides a JSON interface to ROS, allowing any client to send JSON to publish or subscribe to ROS topics, call ROS services, and more. rosbridge supports a variety of transport layers, including WebSockets and TCP. For information on the protocol itself, see the [rosbridge protocol specification](ROSBRIDGE_PROTOCOL.md). 12 | 13 | For full documentation, see [the ROS wiki](http://ros.org/wiki/rosbridge_suite). 14 | 15 | This project is released as part of the [Robot Web Tools](https://robotwebtools.github.io/) effort. 16 | 17 | ### Packages 18 | 19 | * [rosbridge_suite](rosbridge_suite) is a [ROS meta-package](http://www.ros.org/wiki/catkin/conceptual_overview#Metapackages_and_the_Elimination_of_Stacks) including all the rosbridge packages. 20 | 21 | * [rosbridge_library](rosbridge_library) contains the Python API that receives JSON-formatted strings as input and controls ROS publishers/subscribers/service calls according to the content of the JSON strings. 22 | 23 | * [rosbridge_server](rosbridge_server) contains a WebSocket server implementation that exposes the rosbridge_library. 24 | 25 | * [rosapi](rosapi) provides service calls for getting meta-information related to ROS like topic lists as well as interacting with the Parameter Server. 26 | 27 | ### Clients 28 | 29 | A rosbridge client is a program that communicates with rosbridge using its JSON API. rosbridge clients include: 30 | 31 | * [roslibjs](https://github.com/RobotWebTools/roslibjs) - A JavaScript API, which communicates with rosbridge over WebSockets. 32 | * [jrosbridge](https://github.com/WPI-RAIL/jrosbridge) - A Java API, which communicates with rosbridge over WebSockets. 33 | * [roslibpy](https://github.com/gramaziokohler/roslibpy) - A Python API, which communicates with rosbridge over WebSockets. 34 | * [roslibrust](https://github.com/Carter12s/roslibrust) - A Rust API, which communicates with rosbridge over WebSockets. 35 | 36 | ### License 37 | rosbridge_suite is released with a BSD license. For full terms and conditions, see the [LICENSE](LICENSE) file. 38 | 39 | ### Authors 40 | See the [AUTHORS](AUTHORS.md) file for a full list of contributors. 41 | 42 | ### Quality Declaration 43 | This package claims to be in the **Quality Level 3** category, see the [Quality Declaration](/QUALITY_DECLARATION.md) for more details. 44 | 45 | ### Troubleshooting 46 | 47 | See the [TROUBLESHOOTING](TROUBLESHOOTING.md) doc for common problems and solutions. 48 | 49 | ### Release process 50 | 51 | Releasing requires push access to [RobotWebTools/rosbridge_suite](https://github.com/RobotWebTools/rosbridge_suite) as well as [ros2-gbp/rosbridge_suite-release](https://github.com/ros2-gbp/rosbridge_suite-release). For more details on how the release process works, see the [bloom tutorial](https://docs.ros.org/en/galactic/How-To-Guides/Releasing-a-ROS-2-package-with-bloom.html). 52 | 53 | 1. Run `catkin_generate_changelog` to update CHANGELOG.rst files. 54 | 2. Manually edit and clean up the changelogs. Commit the changes. 55 | 3. Run `catkin_prepare_release --bump [major/minor/patch]` to bump versions in package.xml and push changes to origin. 56 | 4. Run bloom-release commands to create PRs to update rosdistro: 57 | - `bloom-release --rosdistro humble rosbridge_suite` 58 | - `bloom-release --rosdistro jazzy rosbridge_suite` 59 | - `bloom-release --rosdistro rolling rosbridge_suite` 60 | 61 | Note that right now, the Humble release is tracked in the `humble` branch, while Jazzy and later are tracked in the `ros2` branch. 62 | 63 | Once the PRs are merged, packages will be available for each distro after the next sync. 64 | Build/sync status can be viewed at: [humble](http://repo.ros2.org/status_page/ros_humble_default.html), [jazzy](http://repo.ros2.org/status_page/ros_jazzy_default.html), and [rolling](http://repo.ros2.org/status_page/ros_rolling_default.html). 65 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | ### Tornado Version 2 | 3 | Often the server breaks because it is using the wrong version of Tornado. Tornado's interfaces and behavior change very quickly, Rosbridge is only tested against the version installed by rosdep. On Debian-based systems, this is the apt package python-tornado. 4 | 5 | To check your imported Tornado version, run: 6 | 7 | ``` 8 | python -c 'import tornado; print tornado.version' 9 | ``` 10 | 11 | The imported version should the `apt` version: 12 | 13 | ``` 14 | apt-cache show python-tornado | grep Version 15 | ``` 16 | 17 | If the versions don't match, it's likely that the wrong version of Tornado was installed with `pip`. Try uninstalling it: 18 | 19 | ``` 20 | pip uninstall tornado 21 | ``` 22 | 23 | If the wrong version is still imported, you will need to find it and remove it. You might find it with something like: 24 | 25 | ``` 26 | find /usr/local/lib/python*/*-packages/ -type d | grep '/tornado/' 27 | ``` 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.bandit] 2 | skips = ["B101", "B110", "B311"] 3 | 4 | [tool.black] 5 | line-length = 100 6 | -------------------------------------------------------------------------------- /rosapi/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rosapi) 3 | 4 | find_package(ament_cmake_ros REQUIRED) 5 | 6 | if(NOT CMAKE_CXX_STANDARD) 7 | set(CMAKE_CXX_STANDARD 14) 8 | endif() 9 | 10 | ament_python_install_package( 11 | ${PROJECT_NAME} PACKAGE_DIR "src/${PROJECT_NAME}") 12 | 13 | ament_package() 14 | 15 | install(PROGRAMS scripts/rosapi_node 16 | DESTINATION lib/${PROJECT_NAME} 17 | ) 18 | 19 | install( 20 | FILES mapping_rules.yaml 21 | DESTINATION share/${PROJECT_NAME} 22 | ) 23 | 24 | if(BUILD_TESTING) 25 | find_package(ament_cmake_pytest REQUIRED) 26 | ament_add_pytest_test(${PROJECT_NAME}_test_stringify_field_types test/test_stringify_field_types.py) 27 | ament_add_pytest_test(${PROJECT_NAME}_test_typedefs test/test_typedefs.py) 28 | 29 | find_package(ament_cmake_mypy REQUIRED) 30 | ament_mypy() 31 | endif() 32 | -------------------------------------------------------------------------------- /rosapi/README.md: -------------------------------------------------------------------------------- 1 | rosapi 2 | =============== 3 | 4 | #### Parameters 5 | 6 | * `~topics_glob` (string, default '') 7 | * `~services_glob` (string, default '') 8 | * `~params_glob` (string, default '') 9 | 10 | ```Note: By default the rosapi calls for details about topics, services, and parameters will return nothing. You must specify a list of allowed resources.``` 11 | Each of the glob parameters may contain an array of one or more match patterns. Resources that match any of the specified patterns will be returned by calls to the rosapi services. 12 | 13 | An example launch file which enables all information to be returned. 14 | 15 | ``` 16 | 17 | 18 | 19 | 20 | 21 | ``` 22 | 23 | 24 | This example launch file enables only rosout and certain camera topics 25 | ``` 26 | 27 | 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /rosapi/mapping_rules.yaml: -------------------------------------------------------------------------------- 1 | - 2 | ros1_package_name: 'rosapi' 3 | ros1_service_name: 'GetParam' 4 | ros2_package_name: 'rosapi' 5 | ros2_service_name: 'GetParam' 6 | request_fields_1_to_2: 7 | name: 'name' 8 | default: 'default_value' 9 | -------------------------------------------------------------------------------- /rosapi/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rosapi 4 | 2.3.0 5 | 6 | Provides service calls for getting ros meta-information, like list of 7 | topics, services, params, etc. 8 | 9 | 10 | BSD 11 | 12 | http://ros.org/wiki/rosapi 13 | https://github.com/RobotWebTools/rosbridge_suite/issues 14 | https://github.com/RobotWebTools/rosbridge_suite 15 | 16 | Jonathan Mace 17 | Błażej Sowa 18 | 19 | ament_cmake_ros 20 | 21 | rosapi_msgs 22 | builtin_interfaces 23 | rclpy 24 | rcl_interfaces 25 | rosbridge_library 26 | ros2node 27 | ros2service 28 | ros2topic 29 | 30 | ament_cmake_mypy 31 | ament_cmake_pytest 32 | sensor_msgs 33 | shape_msgs 34 | geometry_msgs 35 | rmw_dds_common 36 | 37 | 38 | ament_cmake 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /rosapi/src/rosapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosapi/src/rosapi/__init__.py -------------------------------------------------------------------------------- /rosapi/src/rosapi/async_helper.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2025, Fictionlab sp. z o.o. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | from rclpy.node import Node 34 | from rclpy.task import Future 35 | 36 | 37 | async def futures_wait_for(node: Node, futures: list[Future], timeout_sec: float): 38 | """Await a list of futures with a timeout.""" 39 | first_done_future: Future = Future() 40 | 41 | def timeout_callback(): 42 | first_done_future.set_result(None) 43 | 44 | timer = node.create_timer(timeout_sec, timeout_callback) 45 | 46 | def future_done_callback(arg): 47 | if all(future.done() for future in futures): 48 | first_done_future.set_result(None) 49 | 50 | for future in futures: 51 | future.add_done_callback(future_done_callback) 52 | 53 | await first_done_future 54 | 55 | timer.cancel() 56 | timer.destroy() 57 | 58 | 59 | async def async_sleep(node: Node, delay_sec: float): 60 | """Block the coroutine for a given time.""" 61 | sleep_future: Future = Future() 62 | 63 | def timeout_callback(): 64 | sleep_future.set_result(None) 65 | 66 | timer = node.create_timer(delay_sec, timeout_callback) 67 | 68 | await sleep_future 69 | 70 | timer.cancel() 71 | timer.destroy() 72 | -------------------------------------------------------------------------------- /rosapi/src/rosapi/glob_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import fnmatch 4 | from collections import namedtuple 5 | 6 | from rcl_interfaces.msg import ParameterType 7 | 8 | Globs = namedtuple("Globs", ["topics", "services", "params"]) 9 | 10 | 11 | def get_globs(node): 12 | def get_param(parameter_name): 13 | parameter_value = node.get_parameter(parameter_name).get_parameter_value() 14 | if parameter_value.type == ParameterType.PARAMETER_STRING: 15 | parameter_value = parameter_value.string_value 16 | else: 17 | parameter_value = "" 18 | # strips array delimiters in case of an array style value 19 | return [ 20 | element.strip().strip("'") 21 | for element in parameter_value.strip("[").strip("]").split(",") 22 | if len(element.strip().strip("'")) > 0 23 | ] 24 | 25 | topics_glob = get_param("topics_glob") 26 | services_glob = get_param("services_glob") 27 | params_glob = get_param("params_glob") 28 | return Globs(topics_glob, services_glob, params_glob) 29 | 30 | 31 | def filter_globs(globs, full_list): 32 | # If the globs are empty (weren't defined in the params), return the full list 33 | if globs is not None and len(globs) > 0: 34 | return list(filter(lambda x: any_match(x, globs), full_list)) 35 | else: 36 | return full_list 37 | 38 | 39 | def any_match(query, globs): 40 | return ( 41 | globs is None or len(globs) == 0 or any(fnmatch.fnmatch(str(query), glob) for glob in globs) 42 | ) 43 | -------------------------------------------------------------------------------- /rosapi/src/rosapi/stringify_field_types.py: -------------------------------------------------------------------------------- 1 | from rosidl_adapter.parser import parse_message_string 2 | from rosidl_runtime_py import get_interface_path 3 | 4 | 5 | def stringify_field_types(root_type): 6 | definition = "" 7 | seen_types = set() 8 | deps = [root_type] 9 | is_root = True 10 | while deps: 11 | ty = deps.pop() 12 | parts = ty.split("/") 13 | if not is_root: 14 | definition += "\n================================================================================\n" 15 | definition += f"MSG: {ty}\n" 16 | is_root = False 17 | 18 | msg_name = parts[2] if len(parts) == 3 else parts[1] 19 | interface_name = ty if len(parts) == 3 else f"{parts[0]}/msg/{parts[1]}" 20 | with open(get_interface_path(interface_name), encoding="utf-8") as msg_file: 21 | msg_definition = msg_file.read() 22 | definition += msg_definition 23 | 24 | spec = parse_message_string(parts[0], msg_name, msg_definition) 25 | for field in spec.fields: 26 | is_builtin = field.type.pkg_name is None 27 | if not is_builtin: 28 | field_ty = f"{field.type.pkg_name}/{field.type.type}" 29 | if field_ty not in seen_types: 30 | deps.append(field_ty) 31 | seen_types.add(field_ty) 32 | 33 | return definition 34 | -------------------------------------------------------------------------------- /rosapi/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosapi/test/__init__.py -------------------------------------------------------------------------------- /rosapi/test/test_stringify_field_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | 4 | from rosapi.stringify_field_types import stringify_field_types 5 | from rosbridge_library.internal.ros_loader import InvalidModuleException 6 | 7 | 8 | class TestObjectUtils(unittest.TestCase): 9 | def test_stringify_field_types(self): 10 | self.maxDiff = None 11 | 12 | self.assertRegex( 13 | stringify_field_types("std_msgs/String"), 14 | r"(?ms)^string data", 15 | ) 16 | 17 | self.assertRegex( 18 | stringify_field_types("std_msgs/msg/String"), 19 | r"(?ms)^string data", 20 | ) 21 | 22 | self.assertRegex( 23 | stringify_field_types("std_msgs/ByteMultiArray"), 24 | r"""(?s) 25 | MultiArrayLayout layout.* 26 | byte\[\] data.* 27 | 28 | ================================================================================ 29 | MSG: std_msgs/MultiArrayLayout 30 | .* 31 | MultiArrayDimension\[\] dim.* 32 | uint32 data_offset.* 33 | 34 | ================================================================================ 35 | MSG: std_msgs/MultiArrayDimension 36 | .* 37 | string label.* 38 | uint32 size.* 39 | uint32 stride.* 40 | """, 41 | ) 42 | self.assertRegex( 43 | stringify_field_types("sensor_msgs/Image"), 44 | r"""(?s) 45 | std_msgs/Header header.* 46 | .* 47 | uint32 height.* 48 | uint32 width.* 49 | .* 50 | string encoding.* 51 | .* 52 | uint8 is_bigendian.* 53 | uint32 step.* 54 | uint8\[\] data.* 55 | 56 | ================================================================================ 57 | MSG: std_msgs/Header 58 | .* 59 | builtin_interfaces/Time stamp.* 60 | string frame_id 61 | 62 | ================================================================================ 63 | MSG: builtin_interfaces/Time 64 | .* 65 | int32 sec.* 66 | uint32 nanosec 67 | """, 68 | ) 69 | self.assertRegex( 70 | stringify_field_types("sensor_msgs/CameraInfo"), 71 | r"""(?s) 72 | std_msgs/Header header.* 73 | uint32 height.* 74 | uint32 width.* 75 | string distortion_model.* 76 | float64\[\] d.* 77 | float64\[9\] k.* 78 | float64\[9\] r.* 79 | float64\[12\] p.* 80 | uint32 binning_x.* 81 | uint32 binning_y.* 82 | RegionOfInterest roi.* 83 | 84 | ================================================================================ 85 | MSG: sensor_msgs/RegionOfInterest 86 | .* 87 | uint32 x_offset.* 88 | uint32 y_offset.* 89 | uint32 height.* 90 | uint32 width.* 91 | bool do_rectify 92 | 93 | ================================================================================ 94 | MSG: std_msgs/Header 95 | .* 96 | builtin_interfaces/Time stamp.* 97 | string frame_id 98 | 99 | ================================================================================ 100 | MSG: builtin_interfaces/Time 101 | .* 102 | int32 sec.* 103 | uint32 nanosec 104 | """, 105 | ) 106 | 107 | self.assertRegex( 108 | stringify_field_types("shape_msgs/SolidPrimitive"), 109 | r"""(?s) 110 | uint8 BOX=1.* 111 | uint8 SPHERE=2.* 112 | uint8 CYLINDER=3.* 113 | uint8 CONE=4.* 114 | uint8 type.* 115 | float64\[<=3\] dimensions.* 116 | uint8 BOX_X=0.* 117 | uint8 BOX_Y=1.* 118 | uint8 BOX_Z=2.* 119 | uint8 SPHERE_RADIUS=0.* 120 | uint8 CYLINDER_HEIGHT=0.* 121 | uint8 CYLINDER_RADIUS=1.* 122 | uint8 CONE_HEIGHT=0.* 123 | uint8 CONE_RADIUS=1.* 124 | """, 125 | ) 126 | 127 | self.assertEqual( 128 | stringify_field_types("geometry_msgs/Quaternion"), 129 | """\ 130 | # This represents an orientation in free space in quaternion form. 131 | 132 | float64 x 0 133 | float64 y 0 134 | float64 z 0 135 | float64 w 1 136 | """, 137 | ) 138 | 139 | try: 140 | # We match against a regex here as the Gid.msg differs between distros: 141 | # Distros up to humble use 24 bytes, more recent distros use 16 bytes. 142 | # See https://github.com/ros2/rmw_dds_common/pull/68 143 | self.assertRegex( 144 | stringify_field_types("rmw_dds_common/NodeEntitiesInfo"), 145 | r"""string<=256 node_namespace 146 | string<=256 node_name 147 | Gid\[\] reader_gid_seq 148 | Gid\[\] writer_gid_seq 149 | 150 | ================================================================================ 151 | MSG: rmw_dds_common/Gid 152 | char\[(24|16)\] data 153 | """, 154 | ) 155 | except InvalidModuleException: 156 | # This message is not present on older ROS distributions 157 | pass 158 | -------------------------------------------------------------------------------- /rosapi/test/test_typedefs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | 4 | import rosapi.objectutils as objectutils 5 | 6 | # Globally defined ros_loader, used inside the setUp and teardown functions 7 | ros_loader = None 8 | 9 | 10 | class TestUtils(unittest.TestCase): 11 | def setUp(self): 12 | global ros_loader 13 | self.original_ros_loader = ros_loader 14 | ros_loader = self._mock_get_message_instance("default") 15 | 16 | def tearDown(self): 17 | global ros_loader 18 | ros_loader = self.original_ros_loader 19 | 20 | def _mock_get_message_instance(self, type): 21 | class MockInstance(object): 22 | __slots__ = ["_" + type] 23 | _fields_and_field_types = {type: type} 24 | 25 | return MockInstance() 26 | 27 | def test_get_typedef_for_atomic_types(self): 28 | # Test for boolean type 29 | actual_typedef = objectutils.get_typedef("boolean") 30 | # should be None for an atomic 31 | self.assertEqual(actual_typedef, None) 32 | 33 | # Test for float type 34 | actual_typedef = objectutils.get_typedef("float") 35 | # should be None for an atomic 36 | self.assertEqual(actual_typedef, None) 37 | 38 | def test_handle_sequences(self): 39 | # Test for boolean sequence type 40 | actual_typedef = objectutils.get_typedef("sequence") 41 | # should be None for an atomic 42 | self.assertEqual(actual_typedef, None) 43 | 44 | def test_skip_private_slots_in_array_info(self): 45 | # create a fake msg with one real field ('data') and one internal slot 46 | class MockMsg: 47 | __slots__ = ["_check_fields", "_important_data"] 48 | _fields_and_field_types = {"important_data": "int32"} 49 | 50 | def __init__(self): 51 | self._important_data = 123 52 | self._check_fields = None 53 | 54 | inst = MockMsg() 55 | # call the private helper directly 56 | names, types, lens, examples = objectutils._handle_array_information(inst) 57 | 58 | # should only see our single '_important_data' field 59 | self.assertEqual(names, ["important_data"]) 60 | # raw type should be 'int32' (no array) 61 | self.assertEqual(types, ["int32"]) 62 | self.assertEqual(lens, [-1]) 63 | # example should be the stringified value of 123 64 | self.assertEqual(examples, ["123"]) 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /rosapi_msgs/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package rosapi_msgs 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.3.0 (2025-05-19) 6 | ------------------ 7 | * Add services to return Action interface details (`#1021 `_) 8 | * Contributors: David Fernàndez López 9 | 10 | 2.2.0 (2025-02-26) 11 | ------------------ 12 | * New async rosapi params module implementation (`#1001 `_) 13 | * Update maintainers (`#1000 `_) 14 | * Add new service to retrieve the different interfaces in the ROS Network (`#988 `_) 15 | * Contributors: Błażej Sowa, Lebecque Florian 16 | 17 | 2.1.0 (2024-10-08) 18 | ------------------ 19 | 20 | 2.0.0 (2024-10-08) 21 | ------------------ 22 | 23 | 1.3.2 (2023-09-27) 24 | ------------------ 25 | 26 | 1.3.1 (2022-10-21) 27 | ------------------ 28 | 29 | 1.3.0 (2022-08-16) 30 | ------------------ 31 | 32 | 1.2.0 (2022-05-20) 33 | ------------------ 34 | * Added `/rosapi/get_ros_version` service (`#708 `_) 35 | * Contributors: Jacob Bandes-Storch, Kedus Mathewos 36 | 37 | 1.1.2 (2022-01-03) 38 | ------------------ 39 | 40 | 1.1.1 (2021-12-09) 41 | ------------------ 42 | 43 | 1.1.0 (2021-10-22) 44 | ------------------ 45 | * Move msg/srv from rosapi and rosbridge_library into separate packages; enable Rolling in CI (`#665 `_) 46 | * Contributors: Jacob Bandes-Storch 47 | -------------------------------------------------------------------------------- /rosapi_msgs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rosapi_msgs) 3 | 4 | find_package(ament_cmake_ros REQUIRED) 5 | find_package(builtin_interfaces REQUIRED) 6 | find_package(rosidl_default_generators REQUIRED) 7 | 8 | rosidl_generate_interfaces(${PROJECT_NAME} 9 | msg/TypeDef.msg 10 | srv/DeleteParam.srv 11 | srv/GetActionServers.srv 12 | srv/GetParam.srv 13 | srv/GetParamNames.srv 14 | srv/GetROSVersion.srv 15 | srv/GetTime.srv 16 | srv/HasParam.srv 17 | srv/Interfaces.srv 18 | srv/MessageDetails.srv 19 | srv/Nodes.srv 20 | srv/NodeDetails.srv 21 | srv/Publishers.srv 22 | srv/ServiceNode.srv 23 | srv/ServiceProviders.srv 24 | srv/ServiceRequestDetails.srv 25 | srv/ServiceResponseDetails.srv 26 | srv/ActionGoalDetails.srv 27 | srv/ActionResultDetails.srv 28 | srv/ActionFeedbackDetails.srv 29 | srv/Services.srv 30 | srv/ServicesForType.srv 31 | srv/ServiceType.srv 32 | srv/SetParam.srv 33 | srv/Subscribers.srv 34 | srv/Topics.srv 35 | srv/TopicsAndRawTypes.srv 36 | srv/TopicsForType.srv 37 | srv/TopicType.srv 38 | DEPENDENCIES builtin_interfaces 39 | ) 40 | 41 | ament_export_dependencies(builtin_interfaces rosidl_default_runtime) 42 | 43 | ament_package() 44 | -------------------------------------------------------------------------------- /rosapi_msgs/msg/TypeDef.msg: -------------------------------------------------------------------------------- 1 | string type 2 | string[] fieldnames 3 | string[] fieldtypes 4 | int32[] fieldarraylen 5 | string[] examples 6 | string[] constnames 7 | string[] constvalues 8 | -------------------------------------------------------------------------------- /rosapi_msgs/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rosapi_msgs 4 | 2.3.0 5 | 6 | Provides service calls for getting ros meta-information, like list of 7 | topics, services, params, etc. 8 | 9 | 10 | BSD 11 | 12 | http://ros.org/wiki/rosapi 13 | https://github.com/RobotWebTools/rosbridge_suite/issues 14 | https://github.com/RobotWebTools/rosbridge_suite 15 | 16 | Jonathan Mace 17 | Błażej Sowa 18 | 19 | ament_cmake_ros 20 | 21 | rosidl_default_generators 22 | 23 | builtin_interfaces 24 | 25 | builtin_interfaces 26 | rcl_interfaces 27 | rosidl_default_runtime 28 | 29 | rosidl_interface_packages 30 | 31 | 32 | ament_cmake 33 | 34 | 35 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ActionFeedbackDetails.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | TypeDef[] typedefs 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ActionGoalDetails.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | TypeDef[] typedefs 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ActionResultDetails.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | TypeDef[] typedefs 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/DeleteParam.srv: -------------------------------------------------------------------------------- 1 | string name 2 | --- 3 | bool successful 4 | string reason 5 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/GetActionServers.srv: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | string[] action_servers 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/GetParam.srv: -------------------------------------------------------------------------------- 1 | string name 2 | string default_value 3 | --- 4 | string value 5 | bool successful 6 | string reason 7 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/GetParamNames.srv: -------------------------------------------------------------------------------- 1 | --- 2 | string[] names 3 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/GetROSVersion.srv: -------------------------------------------------------------------------------- 1 | --- 2 | uint8 version 3 | string distro 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/GetTime.srv: -------------------------------------------------------------------------------- 1 | --- 2 | builtin_interfaces/Time time 3 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/HasParam.srv: -------------------------------------------------------------------------------- 1 | string name 2 | --- 3 | bool exists 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/Interfaces.srv: -------------------------------------------------------------------------------- 1 | --- 2 | string[] interfaces 3 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/MessageDetails.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | TypeDef[] typedefs 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/NodeDetails.srv: -------------------------------------------------------------------------------- 1 | string node 2 | --- 3 | string[] subscribing 4 | string[] publishing 5 | string[] services 6 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/Nodes.srv: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | string[] nodes 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/Publishers.srv: -------------------------------------------------------------------------------- 1 | string topic 2 | --- 3 | string[] publishers 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ServiceNode.srv: -------------------------------------------------------------------------------- 1 | string service 2 | --- 3 | string node 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ServiceProviders.srv: -------------------------------------------------------------------------------- 1 | string service 2 | --- 3 | string[] providers 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ServiceRequestDetails.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | TypeDef[] typedefs 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ServiceResponseDetails.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | TypeDef[] typedefs 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ServiceType.srv: -------------------------------------------------------------------------------- 1 | string service 2 | --- 3 | string type 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/Services.srv: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | string[] services 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/ServicesForType.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | string[] services 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/SetParam.srv: -------------------------------------------------------------------------------- 1 | string name 2 | string value 3 | --- 4 | bool successful 5 | string reason 6 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/Subscribers.srv: -------------------------------------------------------------------------------- 1 | string topic 2 | --- 3 | string[] subscribers 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/TopicType.srv: -------------------------------------------------------------------------------- 1 | string topic 2 | --- 3 | string type 4 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/Topics.srv: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | string[] topics 4 | string[] types 5 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/TopicsAndRawTypes.srv: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | string[] topics 4 | string[] types 5 | string[] typedefs_full_text 6 | -------------------------------------------------------------------------------- /rosapi_msgs/srv/TopicsForType.srv: -------------------------------------------------------------------------------- 1 | string type 2 | --- 3 | string[] topics 4 | -------------------------------------------------------------------------------- /rosbridge_library/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rosbridge_library) 3 | 4 | find_package(ament_cmake_ros REQUIRED) 5 | find_package(ament_cmake_core REQUIRED) 6 | find_package(ament_cmake_python REQUIRED) 7 | 8 | ament_python_install_package( 9 | ${PROJECT_NAME} PACKAGE_DIR "src/${PROJECT_NAME}") 10 | 11 | ament_package() 12 | 13 | if (BUILD_TESTING) 14 | find_package(ament_cmake_pytest REQUIRED) 15 | ament_add_pytest_test(test_capabilities "test/capabilities/") 16 | ament_add_pytest_test(test_internal "test/internal/") 17 | 18 | find_package(ament_cmake_mypy REQUIRED) 19 | ament_mypy() 20 | endif() 21 | -------------------------------------------------------------------------------- /rosbridge_library/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rosbridge_library 4 | 2.3.0 5 | 6 | The core rosbridge package, responsible for interpreting JSON andperforming 7 | the appropriate ROS action, like subscribe, publish, call service, and 8 | interact with params. 9 | 10 | 11 | BSD 12 | 13 | http://ros.org/wiki/rosbridge_library 14 | https://github.com/RobotWebTools/rosbridge_suite/issues 15 | https://github.com/RobotWebTools/rosbridge_suite 16 | 17 | Jonathan Mace 18 | Błażej Sowa 19 | 20 | ament_cmake 21 | ament_cmake_ros 22 | 23 | python3-pil 24 | python3-bson 25 | 26 | rclpy 27 | python3-pil 28 | rosidl_default_runtime 29 | python3-bson 30 | 31 | rosbridge_test_msgs 32 | action_msgs 33 | ament_cmake_mypy 34 | ament_cmake_pytest 35 | builtin_interfaces 36 | diagnostic_msgs 37 | example_interfaces 38 | control_msgs 39 | geometry_msgs 40 | nav_msgs 41 | sensor_msgs 42 | std_msgs 43 | std_srvs 44 | stereo_msgs 45 | tf2_msgs 46 | trajectory_msgs 47 | visualization_msgs 48 | 49 | 50 | ament_cmake 51 | 52 | 53 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/src/rosbridge_library/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/src/rosbridge_library/capabilities/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/action_feedback.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2023, PickNik Inc. 4 | # 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 the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | from rosbridge_library.capability import Capability 34 | from rosbridge_library.internal import message_conversion, ros_loader 35 | from rosbridge_library.protocol import Protocol 36 | 37 | 38 | class ActionFeedback(Capability): 39 | 40 | action_feedback_msg_fields = [ 41 | (True, "action", str), 42 | (False, "id", str), 43 | (False, "values", dict), 44 | ] 45 | 46 | def __init__(self, protocol: Protocol) -> None: 47 | # Call superclass constructor 48 | Capability.__init__(self, protocol) 49 | 50 | # Register the operations that this capability provides 51 | protocol.register_operation("action_feedback", self.action_feedback) 52 | 53 | def action_feedback(self, message: dict) -> None: 54 | # Typecheck the args 55 | self.basic_type_check(message, self.action_feedback_msg_fields) 56 | 57 | # check for the action 58 | action_name = message["action"] 59 | if action_name in self.protocol.external_action_list: 60 | action_handler = self.protocol.external_action_list[action_name] 61 | # parse the message 62 | goal_id = message["id"] 63 | values = message["values"] 64 | # create a message instance 65 | feedback = ros_loader.get_action_feedback_instance(action_handler.action_type) 66 | message_conversion.populate_instance(values, feedback) 67 | # pass along the feedback 68 | action_handler.handle_feedback(goal_id, feedback) 69 | else: 70 | self.protocol.log( 71 | "error", 72 | f"Action {action_name} has not been advertised via rosbridge.", 73 | ) 74 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/action_result.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2023, PickNik Inc. 4 | # 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 the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | from rosbridge_library.capability import Capability 34 | from rosbridge_library.internal import message_conversion, ros_loader 35 | from rosbridge_library.protocol import Protocol 36 | 37 | 38 | class ActionResult(Capability): 39 | 40 | action_result_msg_fields = [ 41 | (True, "action", str), 42 | (False, "id", str), 43 | (False, "values", dict), 44 | (True, "status", int), 45 | (True, "result", bool), 46 | ] 47 | 48 | def __init__(self, protocol: Protocol) -> None: 49 | # Call superclass constructor 50 | Capability.__init__(self, protocol) 51 | 52 | # Register the operations that this capability provides 53 | protocol.register_operation("action_result", self.action_result) 54 | 55 | def action_result(self, message: dict) -> None: 56 | # Typecheck the args 57 | self.basic_type_check(message, self.action_result_msg_fields) 58 | 59 | # check for the action 60 | action_name = message["action"] 61 | if action_name in self.protocol.external_action_list: 62 | action_handler = self.protocol.external_action_list[action_name] 63 | goal_id = message["id"] 64 | if message["result"]: 65 | # parse the message 66 | values = message["values"] 67 | status = message["status"] 68 | # create a message instance 69 | result = ros_loader.get_action_result_instance(action_handler.action_type) 70 | message_conversion.populate_instance(values, result) 71 | # pass along the result and status 72 | action_handler.handle_result(goal_id, result, status) 73 | else: 74 | # Abort the goal 75 | action_handler.handle_abort(goal_id) 76 | else: 77 | self.protocol.log( 78 | "error", 79 | f"Action {action_name} has not been advertised via rosbridge.", 80 | ) 81 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/advertise_service.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | 3 | import rclpy 4 | from rclpy.callback_groups import ReentrantCallbackGroup 5 | from rosbridge_library.capability import Capability 6 | from rosbridge_library.internal import message_conversion 7 | from rosbridge_library.internal.ros_loader import get_service_class 8 | 9 | 10 | class AdvertisedServiceHandler: 11 | 12 | id_counter = 1 13 | 14 | def __init__(self, service_name, service_type, protocol): 15 | self.request_futures = {} 16 | self.service_name = service_name 17 | self.service_type = service_type 18 | self.protocol = protocol 19 | # setup the service 20 | self.service_handle = protocol.node_handle.create_service( 21 | get_service_class(service_type), 22 | service_name, 23 | self.handle_request, 24 | callback_group=ReentrantCallbackGroup(), # https://github.com/ros2/rclpy/issues/834#issuecomment-961331870 25 | ) 26 | 27 | def next_id(self): 28 | id = self.id_counter 29 | self.id_counter += 1 30 | return id 31 | 32 | async def handle_request(self, req, res): 33 | # generate a unique ID 34 | request_id = f"service_request:{self.service_name}:{self.next_id()}" 35 | 36 | future = rclpy.task.Future() 37 | self.request_futures[request_id] = future 38 | 39 | # build a request to send to the external client 40 | request_message = { 41 | "op": "call_service", 42 | "id": request_id, 43 | "service": self.service_name, 44 | "args": message_conversion.extract_values(req), 45 | } 46 | self.protocol.send(request_message) 47 | 48 | try: 49 | return await future 50 | finally: 51 | del self.request_futures[request_id] 52 | 53 | def handle_response(self, request_id, res): 54 | """ 55 | Called by the ServiceResponse capability to handle a service response from the external client. 56 | """ 57 | if request_id in self.request_futures: 58 | self.request_futures[request_id].set_result(res) 59 | else: 60 | self.protocol.log( 61 | "warning", f"Received service response for unrecognized id: {request_id}" 62 | ) 63 | 64 | def graceful_shutdown(self): 65 | """ 66 | Signal the AdvertisedServiceHandler to shutdown 67 | 68 | Using this, rather than just node_handle.destroy_service, allows us 69 | time to stop any active service requests, ending their busy wait 70 | loops. 71 | """ 72 | if self.request_futures: 73 | incomplete_ids = ", ".join(self.request_futures.keys()) 74 | self.protocol.log( 75 | "warning", 76 | f"Service {self.service_name} was unadvertised with a service call in progress, " 77 | f"aborting service calls with request IDs {incomplete_ids}", 78 | ) 79 | for future_id in self.request_futures: 80 | future = self.request_futures[future_id] 81 | future.set_exception(RuntimeError(f"Service {self.service_name} was unadvertised")) 82 | self.service_handle.destroy() 83 | 84 | 85 | class AdvertiseService(Capability): 86 | services_glob = None 87 | 88 | advertise_service_msg_fields = [(True, "service", str), (True, "type", str)] 89 | 90 | def __init__(self, protocol): 91 | # Call superclass constructor 92 | Capability.__init__(self, protocol) 93 | 94 | # Register the operations that this capability provides 95 | protocol.register_operation("advertise_service", self.advertise_service) 96 | 97 | def advertise_service(self, message): 98 | # Typecheck the args 99 | self.basic_type_check(message, self.advertise_service_msg_fields) 100 | 101 | # parse the incoming message 102 | service_name = message["service"] 103 | 104 | if AdvertiseService.services_glob is not None and AdvertiseService.services_glob: 105 | self.protocol.log( 106 | "debug", 107 | "Service security glob enabled, checking service: " + service_name, 108 | ) 109 | match = False 110 | for glob in AdvertiseService.services_glob: 111 | if fnmatch.fnmatch(service_name, glob): 112 | self.protocol.log( 113 | "debug", 114 | "Found match with glob " + glob + ", continuing service advertisement...", 115 | ) 116 | match = True 117 | break 118 | if not match: 119 | self.protocol.log( 120 | "warn", 121 | "No match found for service, cancelling service advertisement for: " 122 | + service_name, 123 | ) 124 | return 125 | else: 126 | self.protocol.log( 127 | "debug", "No service security glob, not checking service advertisement." 128 | ) 129 | 130 | # check for an existing entry 131 | if service_name in self.protocol.external_service_list.keys(): 132 | self.protocol.log( 133 | "warn", "Duplicate service advertised. Overwriting %s." % service_name 134 | ) 135 | self.protocol.external_service_list[service_name].graceful_shutdown() 136 | del self.protocol.external_service_list[service_name] 137 | 138 | # setup and store the service information 139 | service_type = message["type"] 140 | service_handler = AdvertisedServiceHandler(service_name, service_type, self.protocol) 141 | self.protocol.external_service_list[service_name] = service_handler 142 | self.protocol.log("info", f"Advertised service {service_name}") 143 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/call_service.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | import fnmatch 34 | from functools import partial 35 | from threading import Thread 36 | 37 | from rosbridge_library.capability import Capability 38 | from rosbridge_library.internal.services import ServiceCaller 39 | 40 | 41 | class CallService(Capability): 42 | 43 | call_service_msg_fields = [ 44 | (True, "service", str), 45 | (False, "fragment_size", (int, type(None))), 46 | (False, "compression", str), 47 | ] 48 | 49 | services_glob = None 50 | 51 | def __init__(self, protocol): 52 | # Call superclass constructor 53 | Capability.__init__(self, protocol) 54 | 55 | self.default_timeout = ( 56 | protocol.node_handle.get_parameter("default_call_service_timeout") 57 | .get_parameter_value() 58 | .double_value 59 | ) 60 | 61 | # Register the operations that this capability provides 62 | call_services_in_new_thread = ( 63 | protocol.node_handle.get_parameter("call_services_in_new_thread") 64 | .get_parameter_value() 65 | .bool_value 66 | ) 67 | if call_services_in_new_thread: 68 | # Calls the service in a separate thread so multiple services can be processed simultaneously. 69 | protocol.node_handle.get_logger().info("Calling services in new thread") 70 | protocol.register_operation( 71 | "call_service", lambda msg: Thread(target=self.call_service, args=(msg,)).start() 72 | ) 73 | else: 74 | # Calls the service in this thread, so services block and must be processed sequentially. 75 | protocol.node_handle.get_logger().info("Calling services in existing thread") 76 | protocol.register_operation("call_service", self.call_service) 77 | 78 | def call_service(self, message): 79 | # Pull out the ID 80 | cid = message.get("id", None) 81 | 82 | # Typecheck the args 83 | self.basic_type_check(message, self.call_service_msg_fields) 84 | 85 | # Extract the args 86 | service = message["service"] 87 | fragment_size = message.get("fragment_size", None) 88 | compression = message.get("compression", "none") 89 | args = message.get("args", []) 90 | timeout = message.get("timeout", self.default_timeout) 91 | 92 | if CallService.services_glob is not None and CallService.services_glob: 93 | self.protocol.log( 94 | "debug", "Service security glob enabled, checking service: " + service 95 | ) 96 | match = False 97 | for glob in CallService.services_glob: 98 | if fnmatch.fnmatch(service, glob): 99 | self.protocol.log( 100 | "debug", 101 | "Found match with glob " + glob + ", continuing service call...", 102 | ) 103 | match = True 104 | break 105 | if not match: 106 | self.protocol.log( 107 | "warn", 108 | "No match found for service, cancelling service call for: " + service, 109 | ) 110 | return 111 | else: 112 | self.protocol.log("debug", "No service security glob, not checking service call.") 113 | 114 | # Check for deprecated service ID, eg. /rosbridge/topics#33 115 | cid = extract_id(service, cid) 116 | 117 | # Create the callbacks 118 | s_cb = partial(self._success, cid, service, fragment_size, compression) 119 | e_cb = partial(self._failure, cid, service) 120 | 121 | # Run service caller in the same thread. 122 | ServiceCaller( 123 | trim_servicename(service), 124 | args, 125 | timeout, 126 | s_cb, 127 | e_cb, 128 | self.protocol.node_handle, 129 | ).run() 130 | 131 | def _success(self, cid, service, fragment_size, compression, message): 132 | outgoing_message = { 133 | "op": "service_response", 134 | "service": service, 135 | "values": message, 136 | "result": True, 137 | } 138 | if cid is not None: 139 | outgoing_message["id"] = cid 140 | # TODO: fragmentation, compression 141 | self.protocol.send(outgoing_message) 142 | 143 | def _failure(self, cid, service, exc): 144 | self.protocol.log("error", "call_service %s: %s" % (type(exc).__name__, str(exc)), cid) 145 | # send response with result: false 146 | outgoing_message = { 147 | "op": "service_response", 148 | "service": service, 149 | "values": str(exc), 150 | "result": False, 151 | } 152 | if cid is not None: 153 | outgoing_message["id"] = cid 154 | self.protocol.send(outgoing_message) 155 | 156 | 157 | def trim_servicename(service): 158 | if "#" in service: 159 | return service[: service.find("#")] 160 | return service 161 | 162 | 163 | def extract_id(service, cid): 164 | if cid is not None: 165 | return cid 166 | elif "#" in service: 167 | return service[service.find("#") + 1 :] 168 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/fragmentation.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | import math 34 | 35 | from rosbridge_library.capability import Capability 36 | 37 | 38 | class Fragmentation(Capability): 39 | """The Fragmentation capability doesn't define any incoming operation 40 | handlers, but provides methods to fragment outgoing messages""" 41 | 42 | fragmentation_seed = 0 43 | 44 | def __init__(self, protocol): 45 | # Call superclass constructor 46 | Capability.__init__(self, protocol) 47 | 48 | def fragment(self, message, fragment_size, mid=None): 49 | """Serializes the provided message, then splits the serialized 50 | message according to fragment_size, then sends the fragments. 51 | 52 | If the size of the message is less than the fragment size, then 53 | the original message is returned rather than a single fragment 54 | 55 | Since fragmentation is typically only used for very large messages, 56 | this method returns a generator for fragments rather than a list 57 | 58 | Keyword Arguments 59 | message -- the message dict object to be fragmented 60 | fragment_size -- the max size for the fragments 61 | mid -- (optional) if provided, the fragment messages 62 | will be given this id. Otherwise an id will be auto-generated. 63 | 64 | Returns a generator of message dict objects representing the fragments 65 | """ 66 | # All fragmented messages need an ID so they can be reconstructed 67 | if mid is None: 68 | mid = self.fragmentation_seed 69 | self.fragmentation_seed = self.fragmentation_seed + 1 70 | 71 | serialized = self.protocol.serialize(message, mid) 72 | 73 | if serialized is None: 74 | return [] 75 | 76 | message_length = len(serialized) 77 | if message_length <= fragment_size: 78 | return [message] 79 | 80 | expected_duration = int( 81 | math.ceil(math.ceil(message_length / float(fragment_size))) 82 | * self.protocol.delay_between_messages 83 | ) 84 | 85 | log_msg = ( 86 | "sending " 87 | + str(int(math.ceil(message_length / float(fragment_size)))) 88 | + " parts [fragment size: " 89 | + str(fragment_size) 90 | + "; expected duration: ~" 91 | + str(expected_duration) 92 | + "s]" 93 | ) 94 | self.protocol.log("info", log_msg) 95 | 96 | return self._fragment_generator(serialized, fragment_size, mid) 97 | 98 | def _fragment_generator(self, msg, size, mid): 99 | """Returns a generator of fragment messages""" 100 | total = ((len(msg) - 1) / size) + 1 101 | n = 0 102 | for i in range(0, len(msg), size): 103 | fragment = msg[i : i + size] 104 | yield self._create_fragment(fragment, n, total, mid) 105 | n = n + 1 106 | 107 | def _create_fragment(self, fragment, num, total, mid): 108 | """Given a string fragment of the original message, creates 109 | the appropriate fragment message""" 110 | return { 111 | "op": "fragment", 112 | "id": mid, 113 | "data": fragment, 114 | "num": num, 115 | "total": total, 116 | } 117 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/publish.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # Copyright (c) 2014, Creativa 77 SRL 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software 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 | 34 | import fnmatch 35 | 36 | from rosbridge_library.capability import Capability 37 | from rosbridge_library.internal.publishers import manager 38 | 39 | 40 | class Publish(Capability): 41 | 42 | publish_msg_fields = [(True, "topic", str)] 43 | 44 | topics_glob = None 45 | 46 | def __init__(self, protocol): 47 | # Call superclass constructor 48 | Capability.__init__(self, protocol) 49 | 50 | # Register the operations that this capability provides 51 | protocol.register_operation("publish", self.publish) 52 | 53 | # Save the topics that are published on for the purposes of unregistering 54 | self._published = {} 55 | 56 | if protocol.parameters and "unregister_timeout" in protocol.parameters: 57 | manager.unregister_timeout = protocol.parameters.get("unregister_timeout") 58 | 59 | def publish(self, message): 60 | # Do basic type checking 61 | self.basic_type_check(message, self.publish_msg_fields) 62 | topic = message["topic"] 63 | latch = message.get("latch", False) 64 | queue_size = message.get("queue_size", 100) 65 | 66 | if Publish.topics_glob is not None and Publish.topics_glob: 67 | self.protocol.log("debug", "Topic security glob enabled, checking topic: " + topic) 68 | match = False 69 | for glob in Publish.topics_glob: 70 | if fnmatch.fnmatch(topic, glob): 71 | self.protocol.log( 72 | "debug", 73 | "Found match with glob " + glob + ", continuing publish...", 74 | ) 75 | match = True 76 | break 77 | if not match: 78 | self.protocol.log( 79 | "warn", "No match found for topic, cancelling publish to: " + topic 80 | ) 81 | return 82 | else: 83 | self.protocol.log("debug", "No topic security glob, not checking publish.") 84 | 85 | # Register as a publishing client, propagating any exceptions 86 | client_id = self.protocol.client_id 87 | manager.register( 88 | client_id, 89 | topic, 90 | self.protocol.node_handle, 91 | latch=latch, 92 | queue_size=queue_size, 93 | ) 94 | self._published[topic] = True 95 | 96 | # Get the message if one was provided 97 | msg = message.get("msg", {}) 98 | 99 | # Publish the message 100 | manager.publish( 101 | client_id, 102 | topic, 103 | msg, 104 | self.protocol.node_handle, 105 | latch=latch, 106 | queue_size=queue_size, 107 | ) 108 | 109 | def finish(self): 110 | client_id = self.protocol.client_id 111 | for topic in self._published: 112 | manager.unregister(client_id, topic) 113 | self._published.clear() 114 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/service_response.py: -------------------------------------------------------------------------------- 1 | from rosbridge_library.capability import Capability 2 | from rosbridge_library.internal import message_conversion, ros_loader 3 | 4 | 5 | class ServiceResponse(Capability): 6 | 7 | service_response_msg_fields = [ 8 | (True, "service", str), 9 | (False, "id", str), 10 | (False, "values", dict), 11 | (True, "result", bool), 12 | ] 13 | 14 | def __init__(self, protocol): 15 | # Call superclass constructor 16 | Capability.__init__(self, protocol) 17 | 18 | # Register the operations that this capability provides 19 | protocol.register_operation("service_response", self.service_response) 20 | 21 | def service_response(self, message): 22 | # Typecheck the args 23 | self.basic_type_check(message, self.service_response_msg_fields) 24 | 25 | # check for the service 26 | service_name = message["service"] 27 | if service_name in self.protocol.external_service_list: 28 | service_handler = self.protocol.external_service_list[service_name] 29 | # parse the message 30 | request_id = message["id"] 31 | values = message["values"] 32 | # create a message instance 33 | resp = ros_loader.get_service_response_instance(service_handler.service_type) 34 | message_conversion.populate_instance(values, resp) 35 | # pass along the response 36 | service_handler.handle_response(request_id, resp) 37 | else: 38 | self.protocol.log( 39 | "error", 40 | "Service %s has not been advertised via rosbridge." % service_name, 41 | ) 42 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/unadvertise_action.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2023, PickNik Inc. 4 | # 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 the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | import fnmatch 34 | 35 | from rosbridge_library.capability import Capability 36 | from rosbridge_library.protocol import Protocol 37 | 38 | 39 | class UnadvertiseAction(Capability): 40 | 41 | actions_glob = None 42 | 43 | def __init__(self, protocol: Protocol) -> None: 44 | # Call superclass constructor 45 | Capability.__init__(self, protocol) 46 | 47 | # Register the operations that this capability provides 48 | protocol.register_operation("unadvertise_action", self.unadvertise_action) 49 | 50 | def unadvertise_action(self, message: dict) -> None: 51 | # parse the message 52 | action_name = message["action"] 53 | 54 | if UnadvertiseAction.actions_glob is not None and UnadvertiseAction.actions_glob: 55 | self.protocol.log( 56 | "debug", 57 | f"Action security glob enabled, checking action: {action_name}", 58 | ) 59 | match = False 60 | for glob in UnadvertiseAction.actions_glob: 61 | if fnmatch.fnmatch(action_name, glob): 62 | self.protocol.log( 63 | "debug", 64 | "Found match with glob " + glob + ", continuing action unadvertisement...", 65 | ) 66 | match = True 67 | break 68 | if not match: 69 | self.protocol.log( 70 | "warn", 71 | f"No match found for action, cancelling action unadvertisement for:{action_name}", 72 | ) 73 | return 74 | else: 75 | self.protocol.log( 76 | "debug", 77 | "No action security glob, not checking action unadvertisement...", 78 | ) 79 | 80 | # unregister action in ROS 81 | if action_name in self.protocol.external_action_list.keys(): 82 | self.protocol.external_action_list[action_name].graceful_shutdown() 83 | del self.protocol.external_action_list[action_name] 84 | self.protocol.log("info", f"Unadvertised action {action_name}") 85 | else: 86 | self.protocol.log( 87 | "error", 88 | f"Action {action_name} has not been advertised via rosbridge, can't unadvertise.", 89 | ) 90 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capabilities/unadvertise_service.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | 3 | from rosbridge_library.capability import Capability 4 | 5 | 6 | class UnadvertiseService(Capability): 7 | 8 | # unadvertise_service_msg_fields = [(True, "service", (str, unicode))] 9 | 10 | services_glob = None 11 | 12 | def __init__(self, protocol): 13 | # Call superclass constructor 14 | Capability.__init__(self, protocol) 15 | 16 | # Register the operations that this capability provides 17 | protocol.register_operation("unadvertise_service", self.unadvertise_service) 18 | 19 | def unadvertise_service(self, message): 20 | # parse the message 21 | service_name = message["service"] 22 | 23 | if UnadvertiseService.services_glob is not None and UnadvertiseService.services_glob: 24 | self.protocol.log( 25 | "debug", 26 | "Service security glob enabled, checking service: " + service_name, 27 | ) 28 | match = False 29 | for glob in UnadvertiseService.services_glob: 30 | if fnmatch.fnmatch(service_name, glob): 31 | self.protocol.log( 32 | "debug", 33 | "Found match with glob " + glob + ", continuing service unadvertisement...", 34 | ) 35 | match = True 36 | break 37 | if not match: 38 | self.protocol.log( 39 | "warn", 40 | "No match found for service, cancelling service unadvertisement for: " 41 | + service_name, 42 | ) 43 | return 44 | else: 45 | self.protocol.log( 46 | "debug", 47 | "No service security glob, not checking service unadvertisement...", 48 | ) 49 | 50 | # unregister service in ROS 51 | if service_name in self.protocol.external_service_list.keys(): 52 | self.protocol.external_service_list[service_name].graceful_shutdown() 53 | self.protocol.external_service_list[service_name].service_handle.destroy() 54 | del self.protocol.external_service_list[service_name] 55 | self.protocol.log("info", f"Unadvertised service {service_name}") 56 | else: 57 | self.protocol.log( 58 | "error", 59 | f"Service {service_name} has not been advertised via rosbridge, can't unadvertise.", 60 | ) 61 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/capability.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | from rosbridge_library.internal.exceptions import ( 34 | InvalidArgumentException, 35 | MissingArgumentException, 36 | ) 37 | 38 | 39 | class Capability: 40 | """Handles the operation-specific logic of a rosbridge message 41 | 42 | May define one or more opcodes to handle, for example 'publish' or 43 | 'call_service' 44 | 45 | Each connected client receives its own capability instance, which are 46 | managed by the client's own protocol instance. 47 | 48 | Protocol.send() is available to send messages back to the client. 49 | 50 | """ 51 | 52 | def __init__(self, protocol): 53 | """Abstract class constructor. All capabilities require a handle to 54 | the containing protocol. 55 | 56 | Keyword arguments: 57 | protocol -- the protocol instance for this capability instance 58 | 59 | """ 60 | self.protocol = protocol 61 | 62 | def handle_message(self, message): 63 | """Handle an incoming message. 64 | 65 | Called by the protocol after having already checked the message op code 66 | 67 | Keyword arguments: 68 | message -- the incoming message, deserialized into a dictionary 69 | 70 | """ 71 | pass 72 | 73 | def finish(self): 74 | """Notify this capability that the client is finished and that it's 75 | time to free up resources.""" 76 | pass 77 | 78 | def basic_type_check(self, msg, types_info): 79 | """Performs basic typechecking on fields in msg. 80 | 81 | Keyword arguments: 82 | msg -- a message, deserialized into a dictionary 83 | types_info -- a list of tuples (mandatory, fieldname, fieldtype) where 84 | mandatory - boolean, is the field mandatory 85 | fieldname - the name of the field in the message 86 | fieldtypes - the expected python type of the field or list of types 87 | 88 | Throws: 89 | MissingArgumentException -- if a field is mandatory but not present in 90 | the message 91 | InvalidArgumentException -- if a field is present but not of the type 92 | specified by fieldtype 93 | 94 | """ 95 | for mandatory, fieldname, fieldtypes in types_info: 96 | if mandatory and fieldname not in msg: 97 | raise MissingArgumentException(f"Expected a {fieldname} field but none was found.") 98 | elif fieldname in msg: 99 | if not isinstance(fieldtypes, tuple): 100 | fieldtypes = (fieldtypes,) 101 | valid = False 102 | for typ in fieldtypes: 103 | if isinstance(msg[fieldname], typ): 104 | valid = True 105 | if not valid: 106 | raise InvalidArgumentException( 107 | f"Expected field {fieldname} to be one of {fieldtypes}. Invalid value: {msg[fieldname]}" 108 | ) 109 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/internal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/src/rosbridge_library/internal/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/internal/cbor_conversion.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | try: 4 | from cbor import Tag 5 | except ImportError: 6 | from rosbridge_library.util.cbor import Tag 7 | 8 | 9 | LIST_TYPES = [list, tuple] 10 | INT_TYPES = [ 11 | "byte", 12 | "char", 13 | "int8", 14 | "uint8", 15 | "int16", 16 | "uint16", 17 | "int32", 18 | "uint32", 19 | "int64", 20 | "uint64", 21 | ] 22 | FLOAT_TYPES = ["float", "double"] 23 | STRING_TYPES = ["string"] 24 | BOOL_TYPES = ["boolean"] 25 | TIME_TYPES = ["time", "duration"] 26 | BOOL_ARRAY_TYPES = ["sequence"] 27 | STRING_ARRAY_TYPES = ["sequence"] 28 | BYTESTREAM_TYPES = ["sequence", "sequence"] 29 | 30 | # Typed array tags according to 31 | # Always encode to little-endian variant, for now. 32 | TAGGED_ARRAY_FORMATS = { 33 | "sequence": (69, "<{}H"), 34 | "sequence": (70, "<{}I"), 35 | "sequence": (71, "<{}Q"), 36 | "sequence": (72, "{}b"), 37 | "sequence": (72, "{}b"), 38 | "sequence": (77, "<{}h"), 39 | "sequence": (78, "<{}i"), 40 | "sequence": (79, "<{}q"), 41 | "sequence": (85, "<{}f"), 42 | "sequence": (86, "<{}d"), 43 | } 44 | 45 | 46 | def extract_cbor_values(msg): 47 | """Extract a dictionary of CBOR-friendly values from a ROS message. 48 | 49 | Primitive values will be casted to specific Python primitives. 50 | 51 | Typed arrays will be tagged and packed into byte arrays. 52 | """ 53 | out = {} 54 | for slot, slot_type in msg.get_fields_and_field_types().items(): 55 | val = getattr(msg, slot) 56 | 57 | # string 58 | if slot_type in STRING_TYPES: 59 | out[slot] = str(val) 60 | 61 | # bool 62 | elif slot_type in BOOL_TYPES: 63 | out[slot] = bool(val) 64 | 65 | # integers 66 | elif slot_type in INT_TYPES: 67 | out[slot] = int(val) 68 | 69 | # floats 70 | elif slot_type in FLOAT_TYPES: 71 | out[slot] = float(val) 72 | 73 | # time/duration 74 | elif slot_type in TIME_TYPES: 75 | out[slot] = { 76 | "sec": int(val.sec), 77 | "nanosec": int(val.nanosec), 78 | } 79 | 80 | # byte array 81 | elif slot_type in BYTESTREAM_TYPES: 82 | out[slot] = bytes(val) 83 | 84 | # bool array 85 | elif slot_type in BOOL_ARRAY_TYPES: 86 | out[slot] = [bool(i) for i in val] 87 | 88 | elif slot_type in STRING_ARRAY_TYPES: 89 | out[slot] = [str(i) for i in val] 90 | 91 | # numeric arrays 92 | elif slot_type in TAGGED_ARRAY_FORMATS: 93 | tag, fmt = TAGGED_ARRAY_FORMATS[slot_type] 94 | fmt_to_length = fmt.format(len(val)) 95 | packed = struct.pack(fmt_to_length, *val) 96 | out[slot] = Tag(tag=tag, value=packed) 97 | 98 | # array of messages 99 | elif type(val) in LIST_TYPES: 100 | out[slot] = [extract_cbor_values(i) for i in val] 101 | 102 | # message 103 | else: 104 | out[slot] = extract_cbor_values(val) 105 | 106 | return out 107 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/internal/exceptions.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | 34 | class InvalidArgumentException(Exception): 35 | pass 36 | 37 | 38 | class MissingArgumentException(Exception): 39 | pass 40 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/internal/outgoing_message.py: -------------------------------------------------------------------------------- 1 | from rosbridge_library.internal.cbor_conversion import extract_cbor_values 2 | from rosbridge_library.internal.message_conversion import ( 3 | extract_values as extract_json_values, 4 | ) 5 | 6 | try: 7 | from cbor import dumps as encode_cbor 8 | except ImportError: 9 | from rosbridge_library.util.cbor import dumps as encode_cbor 10 | 11 | 12 | class OutgoingMessage: 13 | """A message wrapper for caching encoding operations.""" 14 | 15 | def __init__(self, message): 16 | self._message = message 17 | self._json_values = None 18 | self._cbor_values = None 19 | self._cbor_msg = None 20 | self._cbor_raw_msg = None 21 | 22 | @property 23 | def message(self): 24 | return self._message 25 | 26 | def get_json_values(self): 27 | if self._json_values is None: 28 | self._json_values = extract_json_values(self._message) 29 | return self._json_values 30 | 31 | def get_cbor_values(self): 32 | if self._cbor_values is None: 33 | self._cbor_values = extract_cbor_values(self._message) 34 | return self._cbor_values 35 | 36 | def get_cbor(self, outgoing_msg): 37 | if self._cbor_msg is None: 38 | outgoing_msg["msg"] = self.get_cbor_values() 39 | self._cbor_msg = encode_cbor(outgoing_msg) 40 | 41 | return self._cbor_msg 42 | 43 | def get_cbor_raw(self, outgoing_msg): 44 | if self._cbor_raw_msg is None: 45 | self._cbor_raw_msg = encode_cbor(outgoing_msg) 46 | 47 | return self._cbor_raw_msg 48 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/internal/pngcompression.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | from base64 import standard_b64decode, standard_b64encode 34 | from io import BytesIO 35 | from math import ceil, floor, sqrt 36 | 37 | from PIL import Image 38 | 39 | 40 | def encode(string): 41 | """PNG-compress the string in a square RGB image padded with '\n', return the b64 encoded bytes""" 42 | string_bytes = string.encode("utf-8") 43 | length = len(string_bytes) 44 | width = floor(sqrt(length / 3.0)) 45 | height = ceil((length / 3.0) / width) 46 | bytes_needed = int(width * height * 3) 47 | string_padded = string_bytes + (b"\n" * (bytes_needed - length)) 48 | i = Image.frombytes("RGB", (int(width), int(height)), string_padded) 49 | buff = BytesIO() 50 | i.save(buff, "png") 51 | encoded = standard_b64encode(buff.getvalue()) 52 | return encoded 53 | 54 | 55 | def decode(string): 56 | """b64 decode the string, then PNG-decompress and remove the '\n' padding""" 57 | decoded = standard_b64decode(string) 58 | buff = BytesIO(decoded) 59 | i = Image.open(buff, formats=("png",)).convert("RGB") 60 | dec_str = i.tobytes().decode("utf-8") 61 | dec_str = dec_str.replace("\n", "") # Remove padding from encoding 62 | return dec_str 63 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/internal/subscription_modifiers.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | import sys 34 | import time 35 | import traceback 36 | from collections import deque 37 | from threading import Condition, Thread 38 | 39 | """ Sits between incoming messages from a subscription, and the outgoing 40 | publish method. Provides throttling / buffering capabilities. 41 | 42 | When the parameters change, the handler may transition to a different kind 43 | of handler 44 | """ 45 | 46 | 47 | class MessageHandler: 48 | def __init__(self, previous_handler=None, publish=None): 49 | if previous_handler: 50 | self.last_publish = previous_handler.last_publish 51 | self.throttle_rate = previous_handler.throttle_rate 52 | self.queue_length = previous_handler.queue_length 53 | self.publish = previous_handler.publish 54 | else: 55 | self.last_publish = 0 56 | self.throttle_rate = 0 57 | self.queue_length = 0 58 | self.publish = publish 59 | 60 | def set_throttle_rate(self, throttle_rate): 61 | self.throttle_rate = throttle_rate / 1000.0 62 | return self.transition() 63 | 64 | def set_queue_length(self, queue_length): 65 | self.queue_length = queue_length 66 | return self.transition() 67 | 68 | def time_remaining(self): 69 | return max((self.last_publish + self.throttle_rate) - time.monotonic(), 0) 70 | 71 | def handle_message(self, msg): 72 | self.last_publish = time.monotonic() 73 | self.publish(msg) 74 | 75 | def transition(self): 76 | if self.throttle_rate == 0 and self.queue_length == 0: 77 | return self 78 | elif self.queue_length == 0: 79 | return ThrottleMessageHandler(self) 80 | else: 81 | return QueueMessageHandler(self) 82 | 83 | def finish(self, block=True): 84 | pass 85 | 86 | 87 | class ThrottleMessageHandler(MessageHandler): 88 | def handle_message(self, msg): 89 | if self.time_remaining() == 0: 90 | MessageHandler.handle_message(self, msg) 91 | 92 | def transition(self): 93 | if self.throttle_rate == 0 and self.queue_length == 0: 94 | return MessageHandler(self) 95 | elif self.queue_length == 0: 96 | return self 97 | else: 98 | return QueueMessageHandler(self) 99 | 100 | def finish(self, block=True): 101 | pass 102 | 103 | 104 | class QueueMessageHandler(MessageHandler, Thread): 105 | def __init__(self, previous_handler): 106 | Thread.__init__(self) 107 | MessageHandler.__init__(self, previous_handler) 108 | self.daemon = True 109 | self.queue = deque(maxlen=self.queue_length) 110 | self.c = Condition() 111 | self.alive = True 112 | self.start() 113 | 114 | def handle_message(self, msg): 115 | with self.c: 116 | if not self.alive: 117 | return 118 | should_notify = len(self.queue) == 0 119 | self.queue.append(msg) 120 | if should_notify: 121 | self.c.notify() 122 | 123 | def transition(self): 124 | if self.throttle_rate == 0 and self.queue_length == 0: 125 | self.finish() 126 | return MessageHandler(self) 127 | elif self.queue_length == 0: 128 | self.finish() 129 | return ThrottleMessageHandler(self) 130 | else: 131 | with self.c: 132 | old_queue = self.queue 133 | self.queue = deque(maxlen=self.queue_length) 134 | while len(old_queue) > 0: 135 | self.queue.append(old_queue.popleft()) 136 | self.c.notify() 137 | return self 138 | 139 | def finish(self, block=True): 140 | """If throttle was set to 0, this pushes all buffered messages""" 141 | # Notify the thread to finish 142 | with self.c: 143 | self.alive = False 144 | self.c.notify() 145 | 146 | if block: 147 | self.join() 148 | 149 | def run(self): 150 | while self.alive: 151 | msg = None 152 | with self.c: 153 | if len(self.queue) == 0: 154 | self.c.wait() 155 | else: 156 | self.c.wait(self.time_remaining()) 157 | if self.alive and self.time_remaining() == 0 and len(self.queue) > 0: 158 | msg = self.queue.popleft() 159 | if msg is not None: 160 | try: 161 | MessageHandler.handle_message(self, msg) 162 | except Exception: 163 | traceback.print_exc(file=sys.stderr) 164 | while self.time_remaining() == 0 and len(self.queue) > 0: 165 | try: 166 | msg = self.queue.popleft() 167 | MessageHandler.handle_message(self, msg) 168 | except Exception: 169 | traceback.print_exc(file=sys.stderr) 170 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/internal/topics.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | """ Exceptions and code common to both publishers and subscribers """ 34 | 35 | 36 | class TopicNotEstablishedException(Exception): 37 | def __init__(self, topic): 38 | Exception.__init__( 39 | self, 40 | "Cannot infer topic type for topic %s as it is not yet advertised" % (topic,), 41 | ) 42 | 43 | 44 | class TypeConflictException(Exception): 45 | def __init__(self, topic, orig_type, new_type): 46 | Exception.__init__( 47 | self, 48 | ( 49 | "Tried to register topic %s with type %s but it is already" 50 | + " established with type %s" 51 | ) 52 | % (topic, new_type, orig_type), 53 | ) 54 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/rosbridge_protocol.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | from rosbridge_library.capabilities.action_feedback import ActionFeedback 34 | from rosbridge_library.capabilities.action_result import ActionResult 35 | from rosbridge_library.capabilities.advertise import Advertise 36 | from rosbridge_library.capabilities.advertise_action import AdvertiseAction 37 | from rosbridge_library.capabilities.advertise_service import AdvertiseService 38 | from rosbridge_library.capabilities.call_service import CallService 39 | from rosbridge_library.capabilities.defragmentation import Defragment 40 | from rosbridge_library.capabilities.publish import Publish 41 | from rosbridge_library.capabilities.send_action_goal import SendActionGoal 42 | from rosbridge_library.capabilities.service_response import ServiceResponse 43 | from rosbridge_library.capabilities.subscribe import Subscribe 44 | from rosbridge_library.capabilities.unadvertise_action import UnadvertiseAction 45 | from rosbridge_library.capabilities.unadvertise_service import UnadvertiseService 46 | from rosbridge_library.protocol import Protocol 47 | 48 | 49 | class RosbridgeProtocol(Protocol): 50 | """Adds the handlers for the rosbridge opcodes""" 51 | 52 | rosbridge_capabilities = [ 53 | Advertise, 54 | Publish, 55 | Subscribe, 56 | Defragment, 57 | AdvertiseService, 58 | CallService, 59 | ServiceResponse, 60 | UnadvertiseService, 61 | AdvertiseAction, 62 | ActionFeedback, 63 | ActionResult, 64 | SendActionGoal, 65 | UnadvertiseAction, 66 | ] 67 | 68 | print("registered capabilities (classes):") 69 | for cap in rosbridge_capabilities: 70 | print(" -", str(cap)) 71 | 72 | parameters = None 73 | 74 | def __init__(self, client_id, node_handle, parameters=None): 75 | self.parameters = parameters 76 | Protocol.__init__(self, client_id, node_handle) 77 | for capability_class in self.rosbridge_capabilities: 78 | self.add_capability(capability_class) 79 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/util/__init__.py: -------------------------------------------------------------------------------- 1 | # try to import json-lib: 1st try ujson, 2nd try simplejson, else import standard Python json 2 | try: 3 | import ujson as json # type: ignore[import-untyped] 4 | except ImportError: 5 | try: 6 | import simplejson as json # type: ignore[import-untyped] 7 | except ImportError: 8 | import json # noqa: F401 9 | 10 | import bson 11 | 12 | try: 13 | bson.BSON 14 | except AttributeError: 15 | raise Exception( 16 | "BSON installation does not support all necessary features. " 17 | "Please use the MongoDB BSON implementation. " 18 | "See: https://github.com/RobotWebTools/rosbridge_suite/issues/198" 19 | ) 20 | -------------------------------------------------------------------------------- /rosbridge_library/src/rosbridge_library/util/ros.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2023, Willow Garage, Inc. 4 | # 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 Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | 34 | def is_topic_published(node, topic_name): 35 | """Checks if a topic is published on a node.""" 36 | published_topic_data = node.get_publisher_names_and_types_by_node( 37 | node.get_name(), node.get_namespace() 38 | ) 39 | return any(topic[0] == topic_name for topic in published_topic_data) 40 | 41 | 42 | def is_topic_subscribed(node, topic_name): 43 | """Checks if a topic is subscribed to by a node.""" 44 | subscribed_topic_data = node.get_subscriber_names_and_types_by_node( 45 | node.get_name(), node.get_namespace() 46 | ) 47 | return any(topic[0] == topic_name for topic in subscribed_topic_data) 48 | -------------------------------------------------------------------------------- /rosbridge_library/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/test/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/test/capabilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/test/capabilities/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/test/capabilities/test_advertise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import unittest 4 | from json import dumps, loads 5 | from threading import Thread 6 | 7 | import rclpy 8 | from rclpy.executors import SingleThreadedExecutor 9 | from rclpy.node import Node 10 | from rosbridge_library.capabilities.advertise import Advertise 11 | from rosbridge_library.internal import ros_loader 12 | from rosbridge_library.internal.exceptions import ( 13 | InvalidArgumentException, 14 | MissingArgumentException, 15 | ) 16 | from rosbridge_library.internal.publishers import manager 17 | from rosbridge_library.protocol import Protocol 18 | from rosbridge_library.util.ros import is_topic_published 19 | 20 | 21 | class TestAdvertise(unittest.TestCase): 22 | def setUp(self): 23 | rclpy.init() 24 | self.executor = SingleThreadedExecutor() 25 | self.node = Node("test_advertise") 26 | self.executor.add_node(self.node) 27 | self.exec_thread = Thread(target=self.executor.spin) 28 | self.exec_thread.start() 29 | 30 | manager.unregister_timeout = 1.0 31 | 32 | def tearDown(self): 33 | self.executor.remove_node(self.node) 34 | self.node.destroy_node() 35 | self.executor.shutdown() 36 | rclpy.shutdown() 37 | 38 | def test_missing_arguments(self): 39 | proto = Protocol("hello", self.node) 40 | adv = Advertise(proto) 41 | msg = {"op": "advertise"} 42 | self.assertRaises(MissingArgumentException, adv.advertise, loads(dumps(msg))) 43 | 44 | msg = {"op": "advertise", "topic": "/jon"} 45 | self.assertRaises(MissingArgumentException, adv.advertise, loads(dumps(msg))) 46 | 47 | msg = {"op": "advertise", "type": "std_msgs/String"} 48 | self.assertRaises(MissingArgumentException, adv.advertise, loads(dumps(msg))) 49 | 50 | def test_invalid_arguments(self): 51 | proto = Protocol("hello", self.node) 52 | adv = Advertise(proto) 53 | 54 | msg = {"op": "advertise", "topic": 3, "type": "std_msgs/String"} 55 | self.assertRaises(InvalidArgumentException, adv.advertise, loads(dumps(msg))) 56 | 57 | msg = {"op": "advertise", "topic": "/jon", "type": 3} 58 | self.assertRaises(InvalidArgumentException, adv.advertise, loads(dumps(msg))) 59 | 60 | def test_invalid_msg_typestrings(self): 61 | invalid = [ 62 | "", 63 | "/", 64 | "//", 65 | "///", 66 | "////", 67 | "/////", 68 | "bad", 69 | "stillbad", 70 | "better/", 71 | "better//", 72 | "better///", 73 | "/better", 74 | "//better", 75 | "///better", 76 | r"this\isbad", 77 | "\\", 78 | ] 79 | 80 | proto = Protocol("hello", self.node) 81 | adv = Advertise(proto) 82 | 83 | for invalid_type in invalid: 84 | msg = { 85 | "op": "advertise", 86 | "topic": "/test_invalid_msg_typestrings", 87 | "type": invalid_type, 88 | } 89 | self.assertRaises( 90 | ros_loader.InvalidTypeStringException, adv.advertise, loads(dumps(msg)) 91 | ) 92 | 93 | def test_invalid_msg_package(self): 94 | nonexistent = [ 95 | "roslib/Time", 96 | "roslib/Duration", 97 | "roslib/Header", 98 | "std_srvs/ConflictedMsg", 99 | "topic_tools/MessageMessage", 100 | "wangle_msgs/Jam", 101 | "whistleblower_msgs/Document", 102 | "coercion_msgs/Bribe", 103 | "airconditioning_msgs/Cold", 104 | "pr2thoughts_msgs/Escape", 105 | ] 106 | 107 | proto = Protocol("hello", self.node) 108 | adv = Advertise(proto) 109 | 110 | for invalid_type in nonexistent: 111 | msg = { 112 | "op": "advertise", 113 | "topic": "/test_invalid_msg_package", 114 | "type": invalid_type, 115 | } 116 | self.assertRaises(ros_loader.InvalidModuleException, adv.advertise, loads(dumps(msg))) 117 | 118 | def test_invalid_msg_classes(self): 119 | nonexistent = [ 120 | "builtin_interfaces/SpaceTime", 121 | "std_msgs/Spool", 122 | "geometry_msgs/Tetrahedron", 123 | "sensor_msgs/TelepathyUnit", 124 | ] 125 | 126 | proto = Protocol("hello", self.node) 127 | adv = Advertise(proto) 128 | 129 | for invalid_type in nonexistent: 130 | msg = { 131 | "op": "advertise", 132 | "topic": "/test_invalid_msg_classes", 133 | "type": invalid_type, 134 | } 135 | self.assertRaises(ros_loader.InvalidClassException, adv.advertise, loads(dumps(msg))) 136 | 137 | def test_valid_msg_classes(self): 138 | assortedmsgs = [ 139 | "geometry_msgs/Pose", 140 | "action_msgs/GoalStatus", 141 | "geometry_msgs/WrenchStamped", 142 | "stereo_msgs/DisparityImage", 143 | "nav_msgs/OccupancyGrid", 144 | "geometry_msgs/Point32", 145 | "std_msgs/String", 146 | "trajectory_msgs/JointTrajectoryPoint", 147 | "diagnostic_msgs/KeyValue", 148 | "visualization_msgs/InteractiveMarkerUpdate", 149 | "nav_msgs/GridCells", 150 | "sensor_msgs/PointCloud2", 151 | ] 152 | 153 | proto = Protocol("hello", self.node) 154 | adv = Advertise(proto) 155 | 156 | for valid_type in assortedmsgs: 157 | msg = {"op": "advertise", "topic": "/" + valid_type, "type": valid_type} 158 | adv.advertise(loads(dumps(msg))) 159 | adv.unadvertise(loads(dumps(msg))) 160 | 161 | def test_do_advertise(self): 162 | proto = Protocol("hello", self.node) 163 | adv = Advertise(proto) 164 | topic = "/test_do_advertise" 165 | type = "std_msgs/String" 166 | 167 | msg = {"op": "advertise", "topic": topic, "type": type} 168 | adv.advertise(loads(dumps(msg))) 169 | self.assertTrue(is_topic_published(self.node, topic)) 170 | adv.unadvertise(loads(dumps(msg))) 171 | self.assertTrue(is_topic_published(self.node, topic)) 172 | time.sleep(manager.unregister_timeout + 1.0) 173 | self.assertFalse(is_topic_published(self.node, topic)) 174 | -------------------------------------------------------------------------------- /rosbridge_library/test/capabilities/test_publish.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import unittest 4 | from json import dumps, loads 5 | from threading import Thread 6 | 7 | import rclpy 8 | from rclpy.executors import SingleThreadedExecutor 9 | from rclpy.node import Node 10 | from rclpy.qos import DurabilityPolicy, QoSProfile 11 | from rosbridge_library.capabilities.publish import Publish 12 | from rosbridge_library.internal.exceptions import ( 13 | InvalidArgumentException, 14 | MissingArgumentException, 15 | ) 16 | from rosbridge_library.protocol import Protocol 17 | from std_msgs.msg import String 18 | 19 | 20 | class TestAdvertise(unittest.TestCase): 21 | def setUp(self): 22 | rclpy.init() 23 | self.executor = SingleThreadedExecutor() 24 | self.node = Node("test_publish") 25 | self.executor.add_node(self.node) 26 | 27 | self.exec_thread = Thread(target=self.executor.spin) 28 | self.exec_thread.start() 29 | 30 | def tearDown(self): 31 | self.executor.remove_node(self.node) 32 | self.node.destroy_node() 33 | self.executor.shutdown() 34 | rclpy.shutdown() 35 | 36 | def test_missing_arguments(self): 37 | proto = Protocol("hello", self.node) 38 | pub = Publish(proto) 39 | msg = {"op": "publish"} 40 | self.assertRaises(MissingArgumentException, pub.publish, msg) 41 | 42 | msg = {"op": "publish", "msg": {}} 43 | self.assertRaises(MissingArgumentException, pub.publish, msg) 44 | 45 | def test_invalid_arguments(self): 46 | proto = Protocol("hello", self.node) 47 | pub = Publish(proto) 48 | 49 | msg = {"op": "publish", "topic": 3} 50 | self.assertRaises(InvalidArgumentException, pub.publish, msg) 51 | 52 | def test_publish_works(self): 53 | proto = Protocol("hello", self.node) 54 | pub = Publish(proto) 55 | topic = "/test_publish_works" 56 | msg = {"data": "test publish works"} 57 | 58 | received = {"msg": None} 59 | 60 | def cb(msg): 61 | received["msg"] = msg 62 | 63 | subscriber_qos = QoSProfile( 64 | depth=10, 65 | durability=DurabilityPolicy.TRANSIENT_LOCAL, 66 | ) 67 | self.node.create_subscription(String, topic, cb, subscriber_qos) 68 | 69 | pub_msg = loads(dumps({"op": "publish", "topic": topic, "msg": msg})) 70 | pub.publish(pub_msg) 71 | time.sleep(0.5) 72 | self.assertEqual(received["msg"].data, msg["data"]) 73 | -------------------------------------------------------------------------------- /rosbridge_library/test/capabilities/test_subscribe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import unittest 4 | from json import dumps, loads 5 | from threading import Thread 6 | 7 | import rclpy 8 | from rclpy.executors import SingleThreadedExecutor 9 | from rclpy.node import Node 10 | from rclpy.qos import DurabilityPolicy, QoSProfile 11 | from rosbridge_library.capabilities import subscribe 12 | from rosbridge_library.internal.exceptions import ( 13 | InvalidArgumentException, 14 | MissingArgumentException, 15 | ) 16 | from rosbridge_library.protocol import Protocol 17 | from std_msgs.msg import String 18 | 19 | 20 | class TestSubscribe(unittest.TestCase): 21 | def setUp(self): 22 | rclpy.init() 23 | self.executor = SingleThreadedExecutor() 24 | self.node = Node("test_subscribe") 25 | self.executor.add_node(self.node) 26 | 27 | self.exec_thread = Thread(target=self.executor.spin) 28 | self.exec_thread.start() 29 | 30 | def tearDown(self): 31 | self.executor.remove_node(self.node) 32 | self.node.destroy_node() 33 | self.executor.shutdown() 34 | rclpy.shutdown() 35 | 36 | def dummy_cb(self, msg): 37 | pass 38 | 39 | def test_update_params(self): 40 | """Adds a bunch of random clients to the subscription and sees whether 41 | the correct parameters are chosen as the min""" 42 | client_id = "client_test_update_params" 43 | topic = "/test_update_params" 44 | msg_type = "std_msgs/String" 45 | 46 | subscription = subscribe.Subscription(client_id, topic, None, self.node) 47 | 48 | min_throttle_rate = 5 49 | min_queue_length = 2 50 | min_frag_size = 20 51 | 52 | for throttle_rate in range(min_throttle_rate, min_throttle_rate + 10): 53 | for queue_length in range(min_queue_length, min_queue_length + 10): 54 | for frag_size in range(min_frag_size, min_frag_size + 10): 55 | sid = throttle_rate * 100 + queue_length * 10 + frag_size 56 | subscription.subscribe(sid, msg_type, throttle_rate, queue_length, frag_size) 57 | 58 | subscription.update_params() 59 | 60 | try: 61 | self.assertEqual(subscription.throttle_rate, min_throttle_rate) 62 | self.assertEqual(subscription.queue_length, min_queue_length) 63 | self.assertEqual(subscription.fragment_size, min_frag_size) 64 | self.assertEqual(subscription.compression, "none") 65 | 66 | list(subscription.clients.values())[0]["compression"] = "png" 67 | 68 | subscription.update_params() 69 | 70 | self.assertEqual(subscription.throttle_rate, min_throttle_rate) 71 | self.assertEqual(subscription.queue_length, min_queue_length) 72 | self.assertEqual(subscription.fragment_size, min_frag_size) 73 | self.assertEqual(subscription.compression, "png") 74 | finally: 75 | subscription.unregister() 76 | 77 | def test_missing_arguments(self): 78 | proto = Protocol("test_missing_arguments", self.node) 79 | sub = subscribe.Subscribe(proto) 80 | msg = {"op": "subscribe"} 81 | self.assertRaises(MissingArgumentException, sub.subscribe, msg) 82 | 83 | def test_invalid_arguments(self): 84 | proto = Protocol("test_invalid_arguments", self.node) 85 | sub = subscribe.Subscribe(proto) 86 | 87 | msg = {"op": "subscribe", "topic": 3} 88 | self.assertRaises(InvalidArgumentException, sub.subscribe, msg) 89 | 90 | msg = {"op": "subscribe", "topic": "/jon", "type": 3} 91 | self.assertRaises(InvalidArgumentException, sub.subscribe, msg) 92 | 93 | msg = {"op": "subscribe", "topic": "/jon", "throttle_rate": "fast"} 94 | self.assertRaises(InvalidArgumentException, sub.subscribe, msg) 95 | 96 | msg = {"op": "subscribe", "topic": "/jon", "fragment_size": "five cubits"} 97 | self.assertRaises(InvalidArgumentException, sub.subscribe, msg) 98 | 99 | msg = {"op": "subscribe", "topic": "/jon", "queue_length": "long"} 100 | self.assertRaises(InvalidArgumentException, sub.subscribe, msg) 101 | 102 | msg = {"op": "subscribe", "topic": "/jon", "compression": 9000} 103 | self.assertRaises(InvalidArgumentException, sub.subscribe, msg) 104 | 105 | def test_subscribe_works(self): 106 | proto = Protocol("test_subscribe_works", self.node) 107 | sub = subscribe.Subscribe(proto) 108 | topic = "/test_subscribe_works" 109 | msg = String() 110 | msg.data = "test test_subscribe_works works" 111 | msg_type = "std_msgs/String" 112 | 113 | received = {"msg": None} 114 | 115 | def send(outgoing, **kwargs): 116 | received["msg"] = outgoing 117 | 118 | proto.send = send 119 | 120 | sub.subscribe(loads(dumps({"op": "subscribe", "topic": topic, "type": msg_type}))) 121 | 122 | publisher_qos = QoSProfile( 123 | depth=10, 124 | durability=DurabilityPolicy.TRANSIENT_LOCAL, 125 | ) 126 | pub = self.node.create_publisher(String, topic, publisher_qos) 127 | pub.publish(msg) 128 | time.sleep(0.1) 129 | self.assertEqual(received["msg"]["msg"]["data"], msg.data) 130 | -------------------------------------------------------------------------------- /rosbridge_library/test/experimental/complex_srv+tcp/test_non-ros_service_client_complex-srv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import socket 3 | 4 | from rosbridge_library.util import json 5 | 6 | # ##################### variables begin ######################################## 7 | # these parameters should be changed to match the actual environment # 8 | # ############################################################################## 9 | 10 | client_socket_timeout = 6 # seconds 11 | max_msg_length = 2000000 # bytes 12 | 13 | rosbridge_ip = "localhost" # hostname or ip 14 | rosbridge_port = 9090 # port as integer 15 | 16 | service_name = "nested_srv" # service name 17 | # request_byte_count = 5000 18 | receiving_fragment_size = 1000 19 | receive_message_intervall = 0.0 20 | 21 | # ##################### variables end ########################################## 22 | 23 | 24 | # ############################################################################## 25 | 26 | 27 | def request_service(): 28 | service_request_object = { 29 | "op": "call_service", # op-code for rosbridge 30 | "service": "/" + service_name, # select service 31 | "fragment_size": receiving_fragment_size, # optional: tells rosbridge to send fragments if message size is bigger than requested 32 | "message_intervall": receive_message_intervall, 33 | "args": { 34 | "pose": { 35 | "position": {"y": 0.0, "x": 0.0, "z": 0.0}, 36 | "orientation": {"y": 0.0, "x": 0.0, "z": 0.0, "w": 0.0}, 37 | } 38 | }, 39 | # "count" : request_byte_count # count is the parameter for send_bytes as defined in srv-file (always put into args field!) 40 | } 41 | service_request = json.dumps(service_request_object) 42 | print("sending JSON-message to rosbridge:", service_request) 43 | sock.send(service_request) 44 | 45 | 46 | # ############################################################################## 47 | 48 | 49 | # ##################### script begin ########################################### 50 | # should not need to be changed (but could be improved ;) ) # 51 | # ############################################################################## 52 | try: 53 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # connect to rosbridge 54 | sock.settimeout(client_socket_timeout) 55 | sock.connect((rosbridge_ip, rosbridge_port)) 56 | 57 | request_service() # send service_request 58 | 59 | incoming = None 60 | buffer = "" 61 | done = False 62 | result = None 63 | reconstructed = None 64 | # should not need a loop (maximum wait can be set by client_socket_timeout), 65 | # but since its for test/demonstration only .. leave it as it is for now 66 | while not done: 67 | try: 68 | incoming = sock.recv(max_msg_length) # receive service_response from rosbridge 69 | if buffer == "": 70 | buffer = incoming.decode("utf-8") 71 | if incoming == "": 72 | print("closing socket") 73 | sock.close() 74 | break 75 | else: 76 | buffer = buffer + incoming.decode("utf-8") 77 | # print "buffer-length:", len(buffer) 78 | try: # try to access service_request directly (not fragmented) 79 | data_object = json.loads(buffer) 80 | if data_object["op"] == "service_response": 81 | reconstructed = buffer 82 | done = True 83 | except Exception: 84 | # print "direct access to JSON failed.." 85 | # print(e) 86 | pass 87 | try: 88 | # print "defragmenting incoming messages" 89 | result_string = buffer.split( 90 | "}{" 91 | ) # split buffer into fragments and re-fill curly brackets 92 | result = [] 93 | for fragment_str in result_string: 94 | if fragment_str[0] != "{": 95 | fragment_str = "{" + fragment_str 96 | if fragment_str[len(fragment_str) - 1] != "}": 97 | fragment_str = fragment_str + "}" 98 | try: 99 | result.append( 100 | json.loads(fragment_str) 101 | ) # try to parse json from string, and append if successful 102 | except Exception: 103 | # print(e) 104 | # print result_string 105 | raise # re-raise the last exception, allows to see and continue with processing of exception 106 | 107 | fragment_count = len(result) 108 | print("fragment_count:", fragment_count) 109 | announced = int(result[0]["total"]) 110 | if fragment_count == announced: # if all fragments received --> sort and defragment 111 | # sort fragments 112 | sorted_result = [None] * fragment_count 113 | unsorted_result = [] 114 | for fragment in result: 115 | unsorted_result.append(fragment) 116 | sorted_result[int(fragment["num"])] = fragment 117 | reconstructed = "" 118 | for fragment in sorted_result: 119 | reconstructed = reconstructed + fragment["data"] 120 | done = True 121 | except Exception: 122 | # print(e) 123 | pass 124 | except Exception: 125 | # print(e) 126 | pass 127 | 128 | returned_data = json.loads( 129 | reconstructed 130 | ) # when service response is received --> access it (as defined in srv-file) 131 | if returned_data["values"] is None: 132 | print("response was None -> service was not available") 133 | else: 134 | print("received:") 135 | print( 136 | returned_data 137 | ) # ["values"]#["data"].decode('base64','strict') # decode values-field 138 | 139 | except Exception as e: 140 | print("ERROR - could not receive service_response") 141 | print(e) 142 | 143 | sock.close() 144 | -------------------------------------------------------------------------------- /rosbridge_library/test/experimental/fragmentation+srv+tcp/test_non-ros_service_client_fragmented.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import socket 3 | from typing import Any 4 | 5 | from rosbridge_library.util import json 6 | 7 | # ##################### variables begin ######################################## 8 | # these parameters should be changed to match the actual environment # 9 | # ############################################################################## 10 | 11 | client_socket_timeout = 6 # seconds 12 | max_msg_length = 2000000 # bytes 13 | 14 | rosbridge_ip = "localhost" # hostname or ip 15 | rosbridge_port = 9090 # port as integer 16 | 17 | service_name = "send_bytes" # service name 18 | request_byte_count = 500000 # NOTE: receiving more than ~100.000 bytes without setting a fragment_size was not possible during testing. 19 | receiving_fragment_size = 1000 20 | receive_message_intervall = 0.0 21 | 22 | # ##################### variables end ########################################## 23 | 24 | 25 | # ############################################################################## 26 | 27 | 28 | def request_service(): 29 | service_request_object = { 30 | "op": "call_service", # op-code for rosbridge 31 | "service": "/" + service_name, # select service 32 | "fragment_size": receiving_fragment_size, # optional: tells rosbridge to send fragments if message size is bigger than requested 33 | "message_intervall": receive_message_intervall, 34 | "args": { 35 | "count": request_byte_count # count is the parameter for send_bytes as defined in srv-file (always put into args field!) 36 | }, 37 | } 38 | service_request = json.dumps(service_request_object) 39 | print("sending JSON-message to rosbridge:", service_request) 40 | sock.send(service_request) 41 | 42 | 43 | # ############################################################################## 44 | 45 | 46 | # ##################### script begin ########################################### 47 | # should not need to be changed (but could be improved ;) ) # 48 | # ############################################################################## 49 | try: 50 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # connect to rosbridge 51 | sock.settimeout(client_socket_timeout) 52 | sock.connect((rosbridge_ip, rosbridge_port)) 53 | 54 | request_service() # send service_request 55 | 56 | incoming = None 57 | buffer = "" 58 | done = False 59 | result = None 60 | reconstructed = None 61 | # should not need a loop (maximum wait can be set by client_socket_timeout), 62 | # but since its for test/demonstration only .. leave it as it is for now 63 | while not done: 64 | try: 65 | incoming = sock.recv(max_msg_length) # receive service_response from rosbridge 66 | if buffer == "": 67 | buffer = incoming.decode("utf-8") 68 | if incoming == "": 69 | print("closing socket") 70 | sock.close() 71 | break 72 | else: 73 | buffer = buffer + incoming.decode("utf-8") 74 | # print "buffer-length:", len(buffer) 75 | try: # try to access service_request directly (not fragmented) 76 | data_object = json.loads(buffer) 77 | if data_object["op"] == "service_response": 78 | reconstructed = buffer 79 | done = True 80 | except Exception: 81 | # print "direct access to JSON failed.." 82 | # print(e) 83 | pass 84 | try: 85 | # print "defragmenting incoming messages" 86 | result_string = buffer.split( 87 | "}{" 88 | ) # split buffer into fragments and re-fill curly brackets 89 | result = [] 90 | for fragment_str in result_string: 91 | if fragment_str[0] != "{": 92 | fragment_str = "{" + fragment_str 93 | if fragment_str[len(fragment_str) - 1] != "}": 94 | fragment_str = fragment_str + "}" 95 | try: 96 | result.append( 97 | json.loads(fragment_str) 98 | ) # try to parse json from string, and append if successful 99 | except Exception: 100 | # print(e) 101 | # print result_string 102 | raise # re-raise the last exception, allows to see and continue with processing of exception 103 | 104 | fragment_count = len(result) 105 | print("fragment_count:", fragment_count) 106 | announced = int(result[0]["total"]) 107 | if fragment_count == announced: # if all fragments received --> sort and defragment 108 | # sort fragments 109 | sorted_result: list[Any] = [None] * fragment_count 110 | unsorted_result = [] 111 | for fragment in result: 112 | unsorted_result.append(fragment) 113 | sorted_result[int(fragment["num"])] = fragment 114 | reconstructed = "" 115 | for fragment in sorted_result: 116 | reconstructed = reconstructed + fragment["data"] 117 | done = True 118 | except Exception: 119 | # print(e) 120 | pass 121 | except Exception: 122 | # print(e) 123 | pass 124 | 125 | returned_data = json.loads( 126 | reconstructed 127 | ) # when service response is received --> access it (as defined in srv-file) 128 | if returned_data["values"] is None: 129 | print("response was None -> service was not available") 130 | else: 131 | print("received:") 132 | print(returned_data["values"]["data"].decode("base64", "strict")) # decode values-field 133 | 134 | except Exception as e: 135 | print("ERROR - could not receive service_response") 136 | print(e) 137 | 138 | sock.close() 139 | -------------------------------------------------------------------------------- /rosbridge_library/test/internal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/test/internal/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/test/internal/publishers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/test/internal/publishers/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/test/internal/publishers/test_multi_publisher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import unittest 4 | from threading import Thread 5 | 6 | import rclpy 7 | from rclpy.executors import MultiThreadedExecutor 8 | from rclpy.node import Node 9 | from rclpy.qos import DurabilityPolicy, QoSProfile 10 | from rosbridge_library.internal import ros_loader 11 | from rosbridge_library.internal.message_conversion import FieldTypeMismatchException 12 | from rosbridge_library.internal.publishers import MultiPublisher 13 | from rosbridge_library.internal.topics import TypeConflictException 14 | from rosbridge_library.util.ros import is_topic_published 15 | 16 | 17 | class TestMultiPublisher(unittest.TestCase): 18 | def setUp(self): 19 | rclpy.init() 20 | self.executor = MultiThreadedExecutor(num_threads=2) 21 | self.node = Node("test_multi_publisher") 22 | self.executor.add_node(self.node) 23 | 24 | self.exec_thread = Thread(target=self.executor.spin) 25 | self.exec_thread.start() 26 | 27 | def tearDown(self): 28 | self.executor.remove_node(self.node) 29 | self.node.destroy_node() 30 | self.executor.shutdown() 31 | rclpy.shutdown() 32 | 33 | def test_register_multipublisher(self): 34 | """Register a publisher on a clean topic with a good msg type""" 35 | topic = "/test_register_multipublisher" 36 | msg_type = "std_msgs/String" 37 | 38 | self.assertFalse(is_topic_published(self.node, topic)) 39 | MultiPublisher(topic, self.node, msg_type) 40 | self.assertTrue(is_topic_published(self.node, topic)) 41 | 42 | def test_unregister_multipublisher(self): 43 | """Register and unregister a publisher on a clean topic with a good msg type""" 44 | topic = "/test_unregister_multipublisher" 45 | msg_type = "std_msgs/String" 46 | 47 | self.assertFalse(is_topic_published(self.node, topic)) 48 | p = MultiPublisher(topic, self.node, msg_type) 49 | self.assertTrue(is_topic_published(self.node, topic)) 50 | p.unregister() 51 | self.assertFalse(is_topic_published(self.node, topic)) 52 | 53 | def test_register_client(self): 54 | """Adds a publisher then removes it.""" 55 | topic = "/test_register_client" 56 | msg_type = "std_msgs/String" 57 | client_id = "client1" 58 | 59 | p = MultiPublisher(topic, self.node, msg_type) 60 | self.assertFalse(p.has_clients()) 61 | 62 | p.register_client(client_id) 63 | self.assertTrue(p.has_clients()) 64 | 65 | p.unregister_client(client_id) 66 | self.assertFalse(p.has_clients()) 67 | 68 | def test_register_multiple_clients(self): 69 | """Adds multiple publishers then removes them.""" 70 | topic = "/test_register_multiple_clients" 71 | msg_type = "std_msgs/String" 72 | 73 | p = MultiPublisher(topic, self.node, msg_type) 74 | self.assertFalse(p.has_clients()) 75 | 76 | for i in range(1000): 77 | p.register_client(f"client{i}") 78 | self.assertTrue(p.has_clients()) 79 | 80 | for i in range(1000): 81 | self.assertTrue(p.has_clients()) 82 | p.unregister_client(f"client{i}") 83 | 84 | self.assertFalse(p.has_clients()) 85 | 86 | def test_verify_type(self): 87 | topic = "/test_verify_type" 88 | msg_type = "std_msgs/String" 89 | othertypes = [ 90 | "geometry_msgs/Pose", 91 | "action_msgs/GoalStatus", 92 | "geometry_msgs/WrenchStamped", 93 | "stereo_msgs/DisparityImage", 94 | "nav_msgs/OccupancyGrid", 95 | "geometry_msgs/Point32", 96 | "trajectory_msgs/JointTrajectoryPoint", 97 | "diagnostic_msgs/KeyValue", 98 | "visualization_msgs/InteractiveMarkerUpdate", 99 | "nav_msgs/GridCells", 100 | "sensor_msgs/PointCloud2", 101 | ] 102 | 103 | p = MultiPublisher(topic, self.node, msg_type) 104 | p.verify_type(msg_type) 105 | for othertype in othertypes: 106 | self.assertRaises(TypeConflictException, p.verify_type, othertype) 107 | 108 | def test_publish(self): 109 | """Make sure that publishing works""" 110 | topic = "/test_publish" 111 | msg_type = "std_msgs/String" 112 | msg = {"data": "why hello there"} 113 | 114 | received = {"msg": None} 115 | 116 | def cb(msg): 117 | received["msg"] = msg 118 | 119 | subscriber_qos = QoSProfile( 120 | depth=10, 121 | durability=DurabilityPolicy.TRANSIENT_LOCAL, 122 | ) 123 | self.node.create_subscription( 124 | ros_loader.get_message_class(msg_type), topic, cb, subscriber_qos 125 | ) 126 | 127 | p = MultiPublisher(topic, self.node, msg_type) 128 | p.publish(msg) 129 | time.sleep(0.1) 130 | self.assertEqual(received["msg"].data, msg["data"]) 131 | 132 | def test_publish_twice(self): 133 | """Make sure that publishing works""" 134 | topic = "/test_publish_twice" 135 | msg_type = "std_msgs/String" 136 | msg = {"data": "why hello there"} 137 | 138 | received = {"msg": None} 139 | 140 | def cb(msg): 141 | received["msg"] = msg 142 | 143 | subscriber_qos = QoSProfile( 144 | depth=10, 145 | durability=DurabilityPolicy.TRANSIENT_LOCAL, 146 | ) 147 | self.node.create_subscription( 148 | ros_loader.get_message_class(msg_type), topic, cb, subscriber_qos 149 | ) 150 | 151 | p = MultiPublisher(topic, self.node, msg_type) 152 | p.publish(msg) 153 | time.sleep(0.1) 154 | self.assertEqual(received["msg"].data, msg["data"]) 155 | 156 | p.unregister() 157 | # The publisher went away at time T. Here's the timeline of the events: 158 | # T+1 seconds - the subscriber will retry to reconnect 159 | # T+2 seconds - publish msg -> it's gone 160 | # T+3 seconds - publish msg -> OK 161 | time.sleep(1) 162 | 163 | received["msg"] = None 164 | self.assertIsNone(received["msg"]) 165 | p = MultiPublisher(topic, self.node, msg_type) 166 | 167 | time.sleep(1) 168 | p.publish(msg) 169 | self.assertIsNone(received["msg"]) 170 | 171 | time.sleep(1) 172 | p.publish(msg) 173 | self.assertEqual(received["msg"].data, msg["data"]) 174 | 175 | def test_bad_publish(self): 176 | """Make sure that bad publishing fails""" 177 | topic = "/test_publish" 178 | msg_type = "std_msgs/String" 179 | msg = {"data": 3} 180 | 181 | p = MultiPublisher(topic, self.node, msg_type) 182 | self.assertRaises(FieldTypeMismatchException, p.publish, msg) 183 | -------------------------------------------------------------------------------- /rosbridge_library/test/internal/subscribers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotWebTools/rosbridge_suite/279f8dd9d690e8dce7fc1b366fb308c9338d8837/rosbridge_library/test/internal/subscribers/__init__.py -------------------------------------------------------------------------------- /rosbridge_library/test/internal/test_compression.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | 4 | from rosbridge_library.internal import pngcompression 5 | 6 | 7 | class TestCompression(unittest.TestCase): 8 | def test_compress(self): 9 | bytes_data = list(range(128)) * 10000 10 | string = str(bytearray(bytes_data)) 11 | encoded = pngcompression.encode(string) 12 | self.assertNotEqual(string, encoded) 13 | 14 | def test_compress_decompress(self): 15 | bytes_data = list(range(128)) * 10000 16 | string = str(bytes(bytes_data)) 17 | encoded = pngcompression.encode(string) 18 | self.assertNotEqual(string, encoded) 19 | decoded = pngcompression.decode(encoded) 20 | self.assertEqual(string, decoded) 21 | -------------------------------------------------------------------------------- /rosbridge_library/test/internal/test_outgoing_message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | 4 | from rosbridge_library.internal.outgoing_message import OutgoingMessage 5 | from std_msgs.msg import String 6 | 7 | 8 | class TestOutgoingMessage(unittest.TestCase): 9 | def test_json_values(self): 10 | msg = String(data="foo") 11 | outgoing = OutgoingMessage(msg) 12 | 13 | result = outgoing.get_json_values() 14 | self.assertEqual(result["data"], msg.data) 15 | 16 | again = outgoing.get_json_values() 17 | self.assertTrue(result is again) 18 | 19 | def test_cbor_values(self): 20 | msg = String(data="foo") 21 | outgoing = OutgoingMessage(msg) 22 | 23 | result = outgoing.get_cbor_values() 24 | self.assertEqual(result["data"], msg.data) 25 | 26 | again = outgoing.get_cbor_values() 27 | self.assertTrue(result is again) 28 | -------------------------------------------------------------------------------- /rosbridge_library/test/test_all.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /rosbridge_msgs/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package rosbridge_msgs 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.3.0 (2025-05-19) 6 | ------------------ 7 | 8 | 2.2.0 (2025-02-26) 9 | ------------------ 10 | * Update maintainers (`#1000 `_) 11 | * Contributors: Błażej Sowa 12 | 13 | 2.1.0 (2024-10-08) 14 | ------------------ 15 | 16 | 2.0.0 (2024-10-08) 17 | ------------------ 18 | 19 | 1.3.2 (2023-09-27) 20 | ------------------ 21 | 22 | 1.3.1 (2022-10-21) 23 | ------------------ 24 | 25 | 1.3.0 (2022-08-16) 26 | ------------------ 27 | 28 | 1.2.0 (2022-05-20) 29 | ------------------ 30 | 31 | 1.1.2 (2022-01-03) 32 | ------------------ 33 | 34 | 1.1.1 (2021-12-09) 35 | ------------------ 36 | 37 | 1.1.0 (2021-10-22) 38 | ------------------ 39 | 40 | 1.0.8 (2021-08-26) 41 | ------------------ 42 | 43 | 1.0.7 (2021-08-18) 44 | ------------------ 45 | 46 | 1.0.6 (2021-08-17) 47 | ------------------ 48 | 49 | 1.0.5 (2021-08-12) 50 | ------------------ 51 | 52 | 1.0.4 (2021-08-11) 53 | ------------------ 54 | 55 | 1.0.3 (2021-08-03) 56 | ------------------ 57 | 58 | 1.0.2 (2019-09-24) 59 | ------------------ 60 | 61 | 1.0.1 (2019-09-20) 62 | ------------------ 63 | 64 | 1.0.0 (2019-09-19) 65 | ------------------ 66 | * Port to ROS 2 67 | 68 | 0.11.3 (2019-08-07) 69 | ------------------- 70 | 71 | 0.11.2 (2019-07-08) 72 | ------------------- 73 | 74 | 0.11.1 (2019-05-08) 75 | ------------------- 76 | 77 | 0.11.0 (2019-03-29) 78 | ------------------- 79 | * Additional client information websocket (`#393 `_) 80 | * Add package rosbridge_msgs. 81 | * rosbridge_server: Publish additional information about connected clients. 82 | * rosbridge_server: Make ClientManager's add_client/remove_client methods thread safe. 83 | * rosbridge_server: Rm unnecessary publishing. 84 | * rosbridge_msgs: Cleanup/fix dependencies. 85 | * Contributors: Hans-Joachim Krauch 86 | -------------------------------------------------------------------------------- /rosbridge_msgs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rosbridge_msgs) 3 | 4 | find_package(ament_cmake_ros REQUIRED) 5 | find_package(builtin_interfaces REQUIRED) 6 | find_package(rosidl_default_generators REQUIRED) 7 | 8 | if(NOT CMAKE_CXX_STANDARD) 9 | set(CMAKE_CXX_STANDARD 14) 10 | endif() 11 | 12 | rosidl_generate_interfaces(${PROJECT_NAME} 13 | msg/ConnectedClient.msg 14 | msg/ConnectedClients.msg 15 | DEPENDENCIES builtin_interfaces 16 | ) 17 | 18 | ament_export_dependencies(rosidl_default_runtime) 19 | 20 | ament_package() 21 | -------------------------------------------------------------------------------- /rosbridge_msgs/msg/ConnectedClient.msg: -------------------------------------------------------------------------------- 1 | string ip_address 2 | builtin_interfaces/Time connection_time 3 | -------------------------------------------------------------------------------- /rosbridge_msgs/msg/ConnectedClients.msg: -------------------------------------------------------------------------------- 1 | ConnectedClient[] clients 2 | -------------------------------------------------------------------------------- /rosbridge_msgs/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rosbridge_msgs 4 | 2.3.0 5 | Package containing message files 6 | 7 | Hans-Joachim Krauch 8 | Błażej Sowa 9 | 10 | BSD 11 | 12 | ament_cmake_ros 13 | rosidl_default_generators 14 | 15 | builtin_interfaces 16 | rosidl_default_runtime 17 | 18 | rosidl_interface_packages 19 | 20 | 21 | ament_cmake 22 | 23 | 24 | -------------------------------------------------------------------------------- /rosbridge_server/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rosbridge_server) 3 | 4 | find_package(ament_cmake_ros REQUIRED) 5 | find_package(ament_cmake_core REQUIRED) 6 | find_package(ament_cmake_python REQUIRED) 7 | 8 | ament_python_install_package( 9 | ${PROJECT_NAME} PACKAGE_DIR "src/${PROJECT_NAME}") 10 | 11 | ament_package() 12 | 13 | install(PROGRAMS 14 | scripts/rosbridge_websocket.py 15 | scripts/rosbridge_websocket 16 | DESTINATION lib/${PROJECT_NAME} 17 | ) 18 | 19 | install(FILES 20 | launch/rosbridge_websocket_launch.xml 21 | DESTINATION share/${PROJECT_NAME}/launch 22 | ) 23 | 24 | if(BUILD_TESTING) 25 | find_package(launch_testing_ament_cmake REQUIRED) 26 | add_launch_test(test/websocket/advertise_action.test.py) 27 | add_launch_test(test/websocket/advertise_action_feedback.test.py) 28 | add_launch_test(test/websocket/advertise_service.test.py) 29 | add_launch_test(test/websocket/call_service.test.py) 30 | add_launch_test(test/websocket/send_action_goal.test.py) 31 | add_launch_test(test/websocket/smoke.test.py) 32 | add_launch_test(test/websocket/transient_local_publisher.test.py) 33 | add_launch_test(test/websocket/best_effort_publisher.test.py) 34 | add_launch_test(test/websocket/multiple_subscribers_raw.test.py) 35 | 36 | find_package(ament_cmake_mypy REQUIRED) 37 | ament_mypy() 38 | endif() 39 | -------------------------------------------------------------------------------- /rosbridge_server/launch/rosbridge_websocket_launch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 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 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /rosbridge_server/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rosbridge_server 4 | 2.3.0 5 | A WebSocket interface to rosbridge. 6 | 7 | BSD 8 | 9 | http://ros.org/wiki/rosbridge_server 10 | https://github.com/RobotWebTools/rosbridge_suite/issues 11 | https://github.com/RobotWebTools/rosbridge_suite 12 | 13 | Jonathan Mace 14 | Błażej Sowa 15 | 16 | ament_cmake 17 | ament_cmake_ros 18 | 19 | python3-tornado 20 | python3-twisted 21 | rclpy 22 | rosbridge_library 23 | rosbridge_msgs 24 | rosapi 25 | 26 | ament_cmake_mypy 27 | example_interfaces 28 | python3-autobahn 29 | launch 30 | launch_ros 31 | launch_testing 32 | launch_testing_ros 33 | launch_testing_ament_cmake 34 | std_srvs 35 | 36 | 37 | ament_cmake 38 | 39 | 40 | -------------------------------------------------------------------------------- /rosbridge_server/scripts/rosbridge_websocket: -------------------------------------------------------------------------------- 1 | rosbridge_websocket.py -------------------------------------------------------------------------------- /rosbridge_server/src/rosbridge_server/__init__.py: -------------------------------------------------------------------------------- 1 | from .client_manager import ClientManager # noqa: F401 2 | from .websocket_handler import RosbridgeWebSocket # noqa: F401 3 | -------------------------------------------------------------------------------- /rosbridge_server/src/rosbridge_server/client_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2018, Intermodalics 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Intermodalics nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software 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 | import threading 34 | 35 | from rclpy.clock import ROSClock 36 | from rclpy.qos import DurabilityPolicy, QoSProfile 37 | from std_msgs.msg import Int32 38 | 39 | from rosbridge_msgs.msg import ConnectedClient, ConnectedClients 40 | 41 | 42 | class ClientManager: 43 | def __init__(self, node_handle): 44 | qos = QoSProfile( 45 | depth=1, 46 | durability=DurabilityPolicy.TRANSIENT_LOCAL, 47 | ) 48 | 49 | # Publisher for number of connected clients 50 | self._client_count_pub = node_handle.create_publisher( 51 | Int32, "client_count", qos_profile=qos 52 | ) 53 | # Publisher for connected clients 54 | self._conn_clients_pub = node_handle.create_publisher( 55 | ConnectedClients, "connected_clients", qos_profile=qos 56 | ) 57 | 58 | self._lock = threading.Lock() 59 | self._client_count = 0 60 | self._clients = {} 61 | self.__publish() 62 | 63 | def __publish(self): 64 | msg = ConnectedClients() 65 | msg.clients = list(self._clients.values()) 66 | self._conn_clients_pub.publish(msg) 67 | self._client_count_pub.publish(Int32(data=len(msg.clients))) 68 | 69 | def add_client(self, client_id, ip_address): 70 | with self._lock: 71 | client = ConnectedClient() 72 | client.ip_address = ip_address 73 | client.connection_time = ROSClock().now().to_msg() 74 | self._clients[client_id] = client 75 | self.__publish() 76 | 77 | def remove_client(self, client_id, ip_address): 78 | with self._lock: 79 | self._clients.pop(client_id, None) 80 | self.__publish() 81 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/advertise_action.test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from action_msgs.msg import GoalStatus 7 | from example_interfaces.action import Fibonacci 8 | from rclpy.action import ActionClient 9 | from rclpy.node import Node 10 | from rclpy.task import Future 11 | from twisted.python import log 12 | 13 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 14 | 15 | import common # noqa: E402 16 | from common import expect_messages, websocket_test # noqa: E402 17 | 18 | log.startLogging(sys.stderr) 19 | 20 | generate_test_description = common.generate_test_description 21 | 22 | 23 | class TestAdvertiseAction(unittest.TestCase): 24 | goal1_result_future: Future | None 25 | goal2_result_future: Future | None 26 | 27 | def goal1_response_callback(self, future): 28 | goal_handle = future.result() 29 | if not goal_handle.accepted: 30 | return 31 | self.goal1_result_future = goal_handle.get_result_async() 32 | 33 | def goal2_response_callback(self, future): 34 | goal_handle = future.result() 35 | if not goal_handle.accepted: 36 | return 37 | self.goal2_result_future = goal_handle.get_result_async() 38 | 39 | @websocket_test 40 | async def test_two_concurrent_calls(self, node: Node, make_client): 41 | ws_client = await make_client() 42 | ws_client.sendJson( 43 | { 44 | "op": "advertise_action", 45 | "action": "/test_fibonacci_action", 46 | "type": "example_interfaces/Fibonacci", 47 | } 48 | ) 49 | client: ActionClient = ActionClient(node, Fibonacci, "/test_fibonacci_action") 50 | client.wait_for_server() 51 | 52 | requests_future, ws_client.message_handler = expect_messages( 53 | 2, "WebSocket", node.get_logger() 54 | ) 55 | assert node.executor is not None 56 | requests_future.add_done_callback(lambda _: node.executor.wake()) 57 | 58 | self.goal1_result_future = None 59 | goal1_future = client.send_goal_async(Fibonacci.Goal(order=3)) 60 | goal1_future.add_done_callback(self.goal1_response_callback) 61 | 62 | self.goal2_result_future = None 63 | goal2_future = client.send_goal_async(Fibonacci.Goal(order=5)) 64 | goal2_future.add_done_callback(self.goal2_response_callback) 65 | 66 | requests = await requests_future 67 | 68 | self.assertEqual(requests[0]["op"], "send_action_goal") 69 | self.assertEqual(requests[0]["action"], "/test_fibonacci_action") 70 | self.assertEqual(requests[0]["action_type"], "example_interfaces/Fibonacci") 71 | self.assertEqual(requests[0]["args"], {"order": 3}) 72 | ws_client.sendJson( 73 | { 74 | "op": "action_result", 75 | "action": "/test_fibonacci_action", 76 | "values": {"sequence": [0, 1, 1, 2]}, 77 | "status": GoalStatus.STATUS_SUCCEEDED, 78 | "id": requests[0]["id"], 79 | "result": True, 80 | } 81 | ) 82 | 83 | self.assertEqual(requests[1]["op"], "send_action_goal") 84 | self.assertEqual(requests[1]["action"], "/test_fibonacci_action") 85 | self.assertEqual(requests[1]["action_type"], "example_interfaces/Fibonacci") 86 | self.assertEqual(requests[1]["args"], {"order": 5}) 87 | ws_client.sendJson( 88 | { 89 | "op": "action_result", 90 | "action": "/test_fibonacci_action", 91 | "values": {"sequence": [0, 1, 1, 2, 3, 5]}, 92 | "status": GoalStatus.STATUS_SUCCEEDED, 93 | "id": requests[1]["id"], 94 | "result": True, 95 | } 96 | ) 97 | 98 | assert self.goal1_result_future is not None 99 | result1 = await self.goal1_result_future 100 | self.assertEqual(result1.result, Fibonacci.Result(sequence=[0, 1, 1, 2])) 101 | assert self.goal2_result_future is not None 102 | result2 = await self.goal2_result_future 103 | self.assertEqual(result2.result, Fibonacci.Result(sequence=[0, 1, 1, 2, 3, 5])) 104 | 105 | node.destroy_client(client) 106 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/advertise_action_feedback.test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from action_msgs.msg import GoalStatus 7 | from example_interfaces.action import Fibonacci 8 | from rclpy.action import ActionClient 9 | from rclpy.node import Node 10 | from rclpy.task import Future 11 | from twisted.python import log 12 | 13 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 14 | 15 | import common # noqa: E402 16 | from common import expect_messages, websocket_test # noqa: E402 17 | 18 | log.startLogging(sys.stderr) 19 | 20 | generate_test_description = common.generate_test_description 21 | 22 | 23 | class TestActionFeedback(unittest.TestCase): 24 | goal_result_future: Future | None 25 | 26 | def goal_response_callback(self, future: Future): 27 | goal_handle = future.result() 28 | assert goal_handle is not None 29 | if not goal_handle.accepted: 30 | return 31 | self.goal_result_future = goal_handle.get_result_async() 32 | 33 | def feedback_callback(self, msg): 34 | self.latest_feedback = msg 35 | 36 | @websocket_test 37 | async def test_feedback(self, node: Node, make_client): 38 | ws_client = await make_client() 39 | ws_client.sendJson( 40 | { 41 | "op": "advertise_action", 42 | "action": "/test_fibonacci_action", 43 | "type": "example_interfaces/Fibonacci", 44 | } 45 | ) 46 | client: ActionClient = ActionClient(node, Fibonacci, "/test_fibonacci_action") 47 | client.wait_for_server() 48 | 49 | requests_future, ws_client.message_handler = expect_messages( 50 | 1, "WebSocket", node.get_logger() 51 | ) 52 | assert node.executor is not None 53 | requests_future.add_done_callback(lambda _: node.executor.wake()) 54 | 55 | self.goal_result_future = None 56 | goal_future = client.send_goal_async( 57 | Fibonacci.Goal(order=5), 58 | feedback_callback=self.feedback_callback, 59 | ) 60 | goal_future.add_done_callback(self.goal_response_callback) 61 | 62 | requests = await requests_future 63 | 64 | self.assertEqual(requests[0]["op"], "send_action_goal") 65 | self.assertEqual(requests[0]["action"], "/test_fibonacci_action") 66 | self.assertEqual(requests[0]["action_type"], "example_interfaces/Fibonacci") 67 | self.assertEqual(requests[0]["args"], {"order": 5}) 68 | 69 | self.latest_feedback = None 70 | ws_client.sendJson( 71 | { 72 | "op": "action_feedback", 73 | "action": "/test_fibonacci_action", 74 | "values": {"sequence": [0, 1, 1, 2]}, 75 | "id": requests[0]["id"], 76 | } 77 | ) 78 | ws_client.sendJson( 79 | { 80 | "op": "action_result", 81 | "action": "/test_fibonacci_action", 82 | "values": {"sequence": [0, 1, 1, 2, 3, 5]}, 83 | "status": GoalStatus.STATUS_SUCCEEDED, 84 | "id": requests[0]["id"], 85 | "result": True, 86 | } 87 | ) 88 | 89 | assert self.goal_result_future is not None 90 | result = await self.goal_result_future 91 | self.assertIsNotNone(self.latest_feedback) 92 | self.assertEqual(self.latest_feedback.feedback, Fibonacci.Feedback(sequence=[0, 1, 1, 2])) 93 | self.assertEqual(result.result, Fibonacci.Result(sequence=[0, 1, 1, 2, 3, 5])) 94 | 95 | node.destroy_client(client) 96 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/advertise_service.test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import unittest 5 | from typing import TYPE_CHECKING 6 | 7 | from rclpy.node import Node 8 | from std_srvs.srv import SetBool 9 | from twisted.python import log 10 | 11 | if TYPE_CHECKING: 12 | from rclpy.client import Client 13 | 14 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 15 | 16 | import common # noqa: E402 17 | from common import expect_messages, websocket_test # noqa: E402 18 | 19 | log.startLogging(sys.stderr) 20 | 21 | generate_test_description = common.generate_test_description 22 | 23 | 24 | class TestAdvertiseService(unittest.TestCase): 25 | @websocket_test 26 | async def test_two_concurrent_calls(self, node: Node, make_client): 27 | ws_client = await make_client() 28 | ws_client.sendJson( 29 | { 30 | "op": "advertise_service", 31 | "type": "std_srvs/SetBool", 32 | "service": "/test_service", 33 | } 34 | ) 35 | client: Client = node.create_client(SetBool, "/test_service") 36 | client.wait_for_service() 37 | 38 | requests_future, ws_client.message_handler = expect_messages( 39 | 2, "WebSocket", node.get_logger() 40 | ) 41 | assert node.executor is not None 42 | requests_future.add_done_callback(lambda _: node.executor.wake()) 43 | 44 | response1_future = client.call_async(SetBool.Request(data=True)) 45 | response2_future = client.call_async(SetBool.Request(data=False)) 46 | 47 | requests = await requests_future 48 | self.assertEqual(len(requests), 2) 49 | 50 | self.assertEqual(requests[0]["op"], "call_service") 51 | self.assertEqual(requests[0]["service"], "/test_service") 52 | self.assertEqual(requests[0]["args"], {"data": True}) 53 | ws_client.sendJson( 54 | { 55 | "op": "service_response", 56 | "service": "/test_service", 57 | "values": {"success": True, "message": "Hello world 1"}, 58 | "id": requests[0]["id"], 59 | "result": True, 60 | } 61 | ) 62 | 63 | self.assertEqual(requests[1]["op"], "call_service") 64 | self.assertEqual(requests[1]["service"], "/test_service") 65 | self.assertEqual(requests[1]["args"], {"data": False}) 66 | ws_client.sendJson( 67 | { 68 | "op": "service_response", 69 | "service": "/test_service", 70 | "values": {"success": True, "message": "Hello world 2"}, 71 | "id": requests[1]["id"], 72 | "result": True, 73 | } 74 | ) 75 | 76 | self.assertEqual( 77 | await response1_future, SetBool.Response(success=True, message="Hello world 1") 78 | ) 79 | self.assertEqual( 80 | await response2_future, SetBool.Response(success=True, message="Hello world 2") 81 | ) 82 | 83 | node.destroy_client(client) 84 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/advertise_service_duplicate.test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | from typing import TYPE_CHECKING 5 | 6 | from rclpy.node import Node 7 | from std_srvs.srv import SetBool 8 | from twisted.python import log 9 | 10 | if TYPE_CHECKING: 11 | from rclpy.client import Client 12 | 13 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 14 | 15 | import common # noqa: E402 16 | from common import expect_messages, sleep, websocket_test # noqa: E402 17 | 18 | log.startLogging(sys.stderr) 19 | 20 | generate_test_description = common.generate_test_description 21 | 22 | 23 | class TestAdvertiseService(unittest.TestCase): 24 | @websocket_test 25 | async def test_double_advertise(self, node: Node, make_client): 26 | ws_client1 = await make_client() 27 | ws_client1.sendJson( 28 | { 29 | "op": "advertise_service", 30 | "type": "std_srvs/SetBool", 31 | "service": "/test_service", 32 | } 33 | ) 34 | client: Client = node.create_client(SetBool, "/test_service") 35 | client.wait_for_service() 36 | 37 | requests1_future, ws_client1.message_handler = expect_messages( 38 | 1, "WebSocket 1", node.get_logger() 39 | ) 40 | assert node.executor is not None 41 | requests1_future.add_done_callback(lambda _: node.executor.wake()) 42 | 43 | client.call_async(SetBool.Request(data=True)) 44 | 45 | requests1 = await requests1_future 46 | self.assertEqual( 47 | requests1, 48 | [ 49 | { 50 | "op": "call_service", 51 | "service": "/test_service", 52 | "id": "service_request:/test_service:1", 53 | "args": {"data": True}, 54 | } 55 | ], 56 | ) 57 | 58 | ws_client1.sendClose() 59 | 60 | ws_client2 = await make_client() 61 | ws_client2.sendJson( 62 | { 63 | "op": "advertise_service", 64 | "type": "std_srvs/SetBool", 65 | "service": "/test_service", 66 | } 67 | ) 68 | 69 | # wait for the server to handle the new advertisement 70 | await sleep(node, 1) 71 | 72 | requests2_future, ws_client2.message_handler = expect_messages( 73 | 1, "WebSocket 2", node.get_logger() 74 | ) 75 | requests2_future.add_done_callback(lambda _: node.executor.wake()) 76 | 77 | response2_future = client.call_async(SetBool.Request(data=False)) 78 | 79 | requests2 = await requests2_future 80 | self.assertEqual( 81 | requests2, 82 | [ 83 | { 84 | "op": "call_service", 85 | "id": "service_request:/test_service:1", 86 | "service": "/test_service", 87 | "args": {"data": False}, 88 | } 89 | ], 90 | ) 91 | 92 | ws_client2.sendJson( 93 | { 94 | "op": "service_response", 95 | "service": "/test_service", 96 | "values": {"success": True, "message": "Hello world 2"}, 97 | "id": "service_request:/test_service:1", 98 | "result": True, 99 | } 100 | ) 101 | 102 | self.assertEqual( 103 | await response2_future, SetBool.Response(success=True, message="Hello world 2") 104 | ) 105 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/best_effort_publisher.test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | from rclpy.node import Node 6 | from rclpy.qos import HistoryPolicy, QoSProfile, ReliabilityPolicy 7 | from std_msgs.msg import String 8 | from twisted.python import log 9 | 10 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 11 | 12 | import common # noqa: E402 13 | from common import expect_messages, sleep, websocket_test # noqa: E402 14 | 15 | log.startLogging(sys.stderr) 16 | 17 | generate_test_description = common.generate_test_description 18 | 19 | 20 | class TestBestEffortPublisher(unittest.TestCase): 21 | @websocket_test 22 | async def test_best_effort_publisher(self, node: Node, make_client): 23 | qos = QoSProfile( 24 | reliability=ReliabilityPolicy.BEST_EFFORT, 25 | history=HistoryPolicy.SYSTEM_DEFAULT, 26 | ) 27 | pub_a = node.create_publisher(String, "/a_topic", qos_profile=qos) 28 | 29 | await sleep(node, 1) # wait for publisher to be set up 30 | 31 | ws_client1 = await make_client() 32 | ws_client1.sendJson( 33 | { 34 | "op": "subscribe", 35 | "topic": "/a_topic", 36 | "type": "std_msgs/String", 37 | } 38 | ) 39 | 40 | await sleep(node, 1) # wait for subscriber to be set up 41 | 42 | pub_a.publish(String(data="hello")) 43 | 44 | ws1_completed_future, ws_client1.message_handler = expect_messages( 45 | 1, "WebSocket 1", node.get_logger() 46 | ) 47 | assert node.executor is not None 48 | ws1_completed_future.add_done_callback(lambda _: node.executor.wake()) 49 | 50 | self.assertEqual( 51 | await ws1_completed_future, 52 | [{"op": "publish", "topic": "/a_topic", "msg": {"data": "hello"}}], 53 | ) 54 | 55 | node.destroy_publisher(pub_a) 56 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/call_service.test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import time 5 | import unittest 6 | 7 | from rclpy.callback_groups import ReentrantCallbackGroup 8 | from rclpy.node import Node 9 | from std_srvs.srv import SetBool 10 | from twisted.python import log 11 | 12 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 13 | 14 | import common # noqa: E402 15 | from common import expect_messages, websocket_test # noqa: E402 16 | 17 | log.startLogging(sys.stderr) 18 | 19 | generate_test_description = common.generate_test_description 20 | 21 | 22 | class TestCallService(unittest.TestCase): 23 | @websocket_test 24 | async def test_one_call(self, node: Node, make_client): 25 | def service_cb(req, res): 26 | self.assertTrue(req.data) 27 | res.success = True 28 | res.message = "Hello, world!" 29 | return res 30 | 31 | service = node.create_service( 32 | SetBool, "/test_service", service_cb, callback_group=ReentrantCallbackGroup() 33 | ) 34 | 35 | ws_client = await make_client() 36 | responses_future, ws_client.message_handler = expect_messages( 37 | 1, "WebSocket", node.get_logger() 38 | ) 39 | assert node.executor is not None 40 | responses_future.add_done_callback(lambda _: node.executor.wake()) 41 | 42 | ws_client.sendJson( 43 | { 44 | "op": "call_service", 45 | "type": "std_srvs/SetBool", 46 | "service": "/test_service", 47 | "args": {"data": True}, 48 | } 49 | ) 50 | 51 | responses = await responses_future 52 | self.assertEqual(len(responses), 1) 53 | self.assertEqual(responses[0]["op"], "service_response") 54 | self.assertEqual(responses[0]["service"], "/test_service") 55 | self.assertEqual(responses[0]["values"], {"success": True, "message": "Hello, world!"}) 56 | self.assertEqual(responses[0]["result"], True) 57 | 58 | node.destroy_service(service) 59 | 60 | def service_long_cb(req, res): 61 | time.sleep(0.2) 62 | self.assertTrue(req.data) 63 | res.success = True 64 | res.message = "Hello, world!" 65 | return res 66 | 67 | service = node.create_service( 68 | SetBool, "/test_service_long", service_long_cb, callback_group=ReentrantCallbackGroup() 69 | ) 70 | 71 | responses_future, ws_client.message_handler = expect_messages( 72 | 2, "WebSocket", node.get_logger() 73 | ) 74 | responses_future.add_done_callback(lambda _: node.executor.wake()) 75 | 76 | ws_client.sendJson( 77 | { 78 | "op": "call_service", 79 | "type": "std_srvs/SetBool", 80 | "service": "/test_service_long", 81 | "args": {"data": True}, 82 | "timeout": 0.1, 83 | } 84 | ) 85 | 86 | ws_client.sendJson( 87 | { 88 | "op": "call_service", 89 | "type": "std_srvs/SetBool", 90 | "service": "/test_service_long", 91 | "args": {"data": True}, 92 | "timeout": 0.5, 93 | } 94 | ) 95 | 96 | responses = await responses_future 97 | self.assertEqual(len(responses), 2) 98 | self.assertEqual(responses[0]["op"], "service_response") 99 | self.assertEqual(responses[0]["service"], "/test_service_long") 100 | self.assertEqual( 101 | responses[0]["values"], "Timeout exceeded while waiting for service response" 102 | ) 103 | self.assertEqual(responses[0]["result"], False) 104 | 105 | self.assertEqual(responses[1]["op"], "service_response") 106 | self.assertEqual(responses[1]["service"], "/test_service_long") 107 | self.assertEqual(responses[1]["values"], {"success": True, "message": "Hello, world!"}) 108 | self.assertEqual(responses[1]["result"], True) 109 | 110 | node.destroy_service(service) 111 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/multiple_subscribers.test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | from rclpy.node import Node 6 | from std_msgs.msg import String 7 | from twisted.python import log 8 | 9 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 10 | 11 | import common # noqa: E402 12 | from common import expect_messages, sleep, websocket_test # noqa: E402 13 | 14 | log.startLogging(sys.stderr) 15 | 16 | generate_test_description = common.generate_test_description 17 | 18 | 19 | class TestMultipleSubscribers(unittest.TestCase): 20 | @websocket_test 21 | async def test_multiple_subscribers(self, node: Node, make_client): 22 | ws_client1 = await make_client() 23 | ws_client1.sendJson( 24 | { 25 | "op": "subscribe", 26 | "topic": "/a_topic", 27 | "type": "std_msgs/String", 28 | } 29 | ) 30 | 31 | ws_client2 = await make_client() 32 | ws_client2.sendJson( 33 | { 34 | "op": "subscribe", 35 | "topic": "/a_topic", 36 | "type": "std_msgs/String", 37 | } 38 | ) 39 | 40 | ws1_completed_future, ws_client1.message_handler = expect_messages( 41 | 1, "WebSocket 1", node.get_logger() 42 | ) 43 | assert node.executor is not None 44 | ws1_completed_future.add_done_callback(lambda _: node.executor.wake()) 45 | ws2_completed_future, ws_client2.message_handler = expect_messages( 46 | 1, "WebSocket 2", node.get_logger() 47 | ) 48 | ws2_completed_future.add_done_callback(lambda _: node.executor.wake()) 49 | 50 | pub_a = node.create_publisher(String, "/a_topic", 1) 51 | 52 | await sleep(node, 1) 53 | 54 | pub_a.publish(String(data="hello")) 55 | 56 | self.assertEqual( 57 | await ws1_completed_future, 58 | [{"op": "publish", "topic": "/a_topic", "msg": {"data": "hello"}}], 59 | ) 60 | self.assertEqual( 61 | await ws2_completed_future, 62 | [{"op": "publish", "topic": "/a_topic", "msg": {"data": "hello"}}], 63 | ) 64 | node.destroy_publisher(pub_a) 65 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/multiple_subscribers_raw.test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | from rclpy.node import Node 6 | from std_msgs.msg import String 7 | from twisted.python import log 8 | 9 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 10 | 11 | import common # noqa: E402 12 | from common import expect_messages, sleep, websocket_test # noqa: E402 13 | 14 | log.startLogging(sys.stderr) 15 | 16 | generate_test_description = common.generate_test_description 17 | 18 | 19 | class TestMultipleSubscribers(unittest.TestCase): 20 | @websocket_test 21 | async def test_multiple_subscribers(self, node: Node, make_client): 22 | sub_operation_json = { 23 | "op": "subscribe", 24 | "topic": "/a_topic", 25 | "type": "std_msgs/String", 26 | "compression": "cbor-raw", 27 | } 28 | ws_client1 = await make_client() 29 | ws_client1.sendJson(sub_operation_json) 30 | ws_client2 = await make_client() 31 | ws_client2.sendJson(sub_operation_json) 32 | 33 | ws1_completed_future, ws_client1.message_handler = expect_messages( 34 | 1, "WebSocket 1", node.get_logger() 35 | ) 36 | assert node.executor is not None 37 | ws1_completed_future.add_done_callback(lambda _: node.executor.wake()) 38 | ws2_completed_future, ws_client2.message_handler = expect_messages( 39 | 1, "WebSocket 2", node.get_logger() 40 | ) 41 | ws2_completed_future.add_done_callback(lambda _: node.executor.wake()) 42 | 43 | pub_a = node.create_publisher(String, "/a_topic", 1) 44 | 45 | await sleep(node, 1) 46 | pub_a.publish(String(data="hello")) 47 | await sleep(node, 1) 48 | 49 | self.assertTrue(await ws1_completed_future) 50 | self.assertTrue(await ws2_completed_future) 51 | node.destroy_publisher(pub_a) 52 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/send_action_goal.test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import time 5 | import unittest 6 | 7 | from action_msgs.msg import GoalStatus 8 | from example_interfaces.action import Fibonacci 9 | from rclpy.action import ActionServer, CancelResponse 10 | from rclpy.callback_groups import ReentrantCallbackGroup 11 | from rclpy.node import Node 12 | from twisted.python import log 13 | 14 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 15 | 16 | import common # noqa: E402 17 | from common import expect_messages, websocket_test # noqa: E402 18 | 19 | log.startLogging(sys.stderr) 20 | 21 | generate_test_description = common.generate_test_description 22 | 23 | 24 | class TestSendActionGoal(unittest.TestCase): 25 | def cancel_callback(self, _): 26 | return CancelResponse.ACCEPT 27 | 28 | def execute_callback(self, goal): 29 | feedback_msg = Fibonacci.Feedback() 30 | feedback_msg.sequence = [0, 1] 31 | 32 | for i in range(1, goal.request.order): 33 | if goal.is_cancel_requested: 34 | goal.canceled() 35 | return Fibonacci.Result() 36 | 37 | feedback_msg.sequence.append(feedback_msg.sequence[i] + feedback_msg.sequence[i - 1]) 38 | goal.publish_feedback(feedback_msg) 39 | time.sleep(0.5) 40 | 41 | goal.succeed() 42 | return Fibonacci.Result(sequence=feedback_msg.sequence) 43 | 44 | @websocket_test 45 | async def test_one_call(self, node: Node, make_client): 46 | action_server = ActionServer( 47 | node, 48 | Fibonacci, 49 | "/test_fibonacci_action", 50 | execute_callback=self.execute_callback, 51 | cancel_callback=self.cancel_callback, 52 | callback_group=ReentrantCallbackGroup(), 53 | ) 54 | 55 | ws_client = await make_client() 56 | responses_future, ws_client.message_handler = expect_messages( 57 | 5, "WebSocket", node.get_logger() 58 | ) 59 | assert node.executor is not None 60 | responses_future.add_done_callback(lambda _: node.executor.wake()) 61 | 62 | ws_client.sendJson( 63 | { 64 | "op": "send_action_goal", 65 | "action": "/test_fibonacci_action", 66 | "action_type": "example_interfaces/Fibonacci", 67 | "args": {"order": 5}, 68 | "feedback": True, 69 | } 70 | ) 71 | 72 | responses = await responses_future 73 | expected_result = [0, 1, 1, 2, 3, 5] 74 | self.assertEqual(len(responses), 5) 75 | 76 | for idx in range(4): 77 | self.assertEqual(responses[idx]["op"], "action_feedback") 78 | self.assertEqual(responses[idx]["values"]["sequence"], expected_result[: idx + 3]) 79 | 80 | self.assertEqual(responses[-1]["op"], "action_result") 81 | self.assertEqual(responses[-1]["action"], "/test_fibonacci_action") 82 | self.assertEqual(responses[-1]["values"]["sequence"], expected_result) 83 | self.assertEqual(responses[-1]["status"], GoalStatus.STATUS_SUCCEEDED) 84 | self.assertEqual(responses[-1]["result"], True) 85 | 86 | action_server.destroy() 87 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/smoke.test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from rclpy.node import Node 7 | from std_msgs.msg import String 8 | from twisted.python import log 9 | 10 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 11 | 12 | import common # noqa: E402 13 | from common import expect_messages, sleep, websocket_test # noqa: E402 14 | 15 | log.startLogging(sys.stderr) 16 | 17 | generate_test_description = common.generate_test_description 18 | 19 | 20 | class TestWebsocketSmoke(unittest.TestCase): 21 | @websocket_test 22 | async def test_smoke(self, node: Node, make_client): 23 | ws_client = await make_client() 24 | # For consistency, the number of messages must not exceed the the protocol 25 | # Subscriber queue_size. 26 | NUM_MSGS = 10 27 | MSG_SIZE = 10 28 | A_TOPIC = "/a_topic" 29 | B_TOPIC = "/b_topic" 30 | A_STRING = "A" * MSG_SIZE 31 | B_STRING = "B" * MSG_SIZE 32 | WARMUP_DELAY = 1.0 # seconds 33 | 34 | sub_completed_future, sub_handler = expect_messages( 35 | NUM_MSGS, "ROS subscriber", node.get_logger() 36 | ) 37 | ws_completed_future, ws_client.message_handler = expect_messages( 38 | NUM_MSGS, "WebSocket", node.get_logger() 39 | ) 40 | assert node.executor is not None 41 | ws_completed_future.add_done_callback(lambda _: node.executor.wake()) 42 | 43 | sub_a = node.create_subscription(String, A_TOPIC, sub_handler, NUM_MSGS) 44 | pub_b = node.create_publisher(String, B_TOPIC, NUM_MSGS) 45 | 46 | ws_client.sendJson( 47 | { 48 | "op": "subscribe", 49 | "topic": B_TOPIC, 50 | "type": "std_msgs/String", 51 | "queue_length": 0, # Test the roslib default. 52 | } 53 | ) 54 | ws_client.sendJson( 55 | { 56 | "op": "advertise", 57 | "topic": A_TOPIC, 58 | "type": "std_msgs/String", 59 | } 60 | ) 61 | 62 | await sleep(node, WARMUP_DELAY) 63 | 64 | ws_client.sendJson( 65 | { 66 | "op": "publish", 67 | "topic": A_TOPIC, 68 | "msg": { 69 | "data": A_STRING, 70 | }, 71 | }, 72 | times=NUM_MSGS, 73 | ) 74 | 75 | for _ in range(NUM_MSGS): 76 | pub_b.publish(String(data=B_STRING)) 77 | 78 | ros_received = await sub_completed_future 79 | ws_received = await ws_completed_future 80 | 81 | for msg in ws_received: 82 | self.assertEqual("publish", msg["op"]) 83 | self.assertEqual(B_TOPIC, msg["topic"]) 84 | self.assertEqual(B_STRING, msg["msg"]["data"]) 85 | self.assertEqual(NUM_MSGS, len(ws_received)) 86 | 87 | for msg in ros_received: 88 | self.assertEqual(A_STRING, msg.data) 89 | self.assertEqual(NUM_MSGS, len(ros_received)) 90 | 91 | node.destroy_subscription(sub_a) 92 | node.destroy_publisher(pub_b) 93 | -------------------------------------------------------------------------------- /rosbridge_server/test/websocket/transient_local_publisher.test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | from rclpy.node import Node 6 | from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile 7 | from std_msgs.msg import String 8 | from twisted.python import log 9 | 10 | sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory 11 | 12 | import common # noqa: E402 13 | from common import expect_messages, sleep, websocket_test # noqa: E402 14 | 15 | log.startLogging(sys.stderr) 16 | 17 | generate_test_description = common.generate_test_description 18 | 19 | 20 | class TestTransientLocalPublisher(unittest.TestCase): 21 | @websocket_test 22 | async def test_transient_local_publisher(self, node: Node, make_client): 23 | qos = QoSProfile( 24 | depth=1, 25 | durability=DurabilityPolicy.TRANSIENT_LOCAL, 26 | history=HistoryPolicy.KEEP_LAST, 27 | ) 28 | pub_a = node.create_publisher(String, "/a_topic", qos_profile=qos) 29 | 30 | await sleep(node, 1) 31 | 32 | pub_a.publish(String(data="hello")) 33 | 34 | await sleep(node, 1) 35 | 36 | for num in range(3): 37 | ws_client = await make_client() 38 | ws_client.sendJson( 39 | { 40 | "op": "subscribe", 41 | "topic": "/a_topic", 42 | "type": "std_msgs/String", 43 | } 44 | ) 45 | 46 | ws_completed_future, ws_client.message_handler = expect_messages( 47 | 1, "WebSocket " + str(num), node.get_logger() 48 | ) 49 | assert node.executor is not None 50 | ws_completed_future.add_done_callback(lambda _: node.executor.wake()) 51 | 52 | self.assertEqual( 53 | await ws_completed_future, 54 | [{"op": "publish", "topic": "/a_topic", "msg": {"data": "hello"}}], 55 | ) 56 | 57 | node.destroy_publisher(pub_a) 58 | -------------------------------------------------------------------------------- /rosbridge_suite/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package rosbridge_suite 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.3.0 (2025-05-19) 6 | ------------------ 7 | 8 | 2.2.0 (2025-02-26) 9 | ------------------ 10 | * Update maintainers (`#1000 `_) 11 | * Contributors: Błażej Sowa 12 | 13 | 2.1.0 (2024-10-08) 14 | ------------------ 15 | 16 | 2.0.0 (2024-10-08) 17 | ------------------ 18 | 19 | 1.3.2 (2023-09-27) 20 | ------------------ 21 | * rosbridge_suite: depend on ament_cmake (`#848 `_) 22 | * Contributors: Jochen Sprickerhof 23 | 24 | 1.3.1 (2022-10-21) 25 | ------------------ 26 | 27 | 1.3.0 (2022-08-16) 28 | ------------------ 29 | 30 | 1.2.0 (2022-05-20) 31 | ------------------ 32 | 33 | 1.1.2 (2022-01-03) 34 | ------------------ 35 | 36 | 1.1.1 (2021-12-09) 37 | ------------------ 38 | 39 | 1.1.0 (2021-10-22) 40 | ------------------ 41 | 42 | 1.0.8 (2021-08-26) 43 | ------------------ 44 | 45 | 1.0.7 (2021-08-18) 46 | ------------------ 47 | * Fix typos discovered by codespell (`#600 `_) 48 | * Contributors: Christian Clauss 49 | 50 | 1.0.6 (2021-08-17) 51 | ------------------ 52 | 53 | 1.0.5 (2021-08-12) 54 | ------------------ 55 | 56 | 1.0.4 (2021-08-11) 57 | ------------------ 58 | 59 | 1.0.3 (2021-08-03) 60 | ------------------ 61 | 62 | 1.0.2 (2019-09-24) 63 | ------------------ 64 | 65 | 1.0.1 (2019-09-20) 66 | ------------------ 67 | 68 | 1.0.0 (2019-09-19) 69 | ------------------ 70 | * Port to ROS 2 71 | 72 | 0.11.3 (2019-08-07) 73 | ------------------- 74 | 75 | 0.11.2 (2019-07-08) 76 | ------------------- 77 | 78 | 0.11.1 (2019-05-08) 79 | ------------------- 80 | 81 | 0.11.0 (2019-03-29) 82 | ------------------- 83 | 84 | 0.10.2 (2019-03-04) 85 | ------------------- 86 | 87 | 0.10.1 (2018-12-16) 88 | ------------------- 89 | 90 | 0.10.0 (2018-12-14) 91 | ------------------- 92 | * use package format 2, remove unnecessary dependencies (`#348 `_) 93 | * Contributors: Dirk Thomas 94 | 95 | 0.9.0 (2018-04-09) 96 | ------------------ 97 | 98 | 0.8.6 (2017-12-08) 99 | ------------------ 100 | 101 | 0.8.5 (2017-11-23) 102 | ------------------ 103 | 104 | 0.8.4 (2017-10-16) 105 | ------------------ 106 | 107 | 0.8.3 (2017-09-11) 108 | ------------------ 109 | 110 | 0.8.2 (2017-09-11) 111 | ------------------ 112 | 113 | 0.8.1 (2017-08-30) 114 | ------------------ 115 | 116 | 0.8.0 (2017-08-30) 117 | ------------------ 118 | 119 | 0.7.17 (2017-01-25) 120 | ------------------- 121 | 122 | 0.7.16 (2016-08-15) 123 | ------------------- 124 | 125 | 0.7.15 (2016-04-25) 126 | ------------------- 127 | * changelog updated 128 | * Contributors: Russell Toris 129 | 130 | 0.7.14 (2016-02-11) 131 | ------------------- 132 | 133 | 0.7.13 (2015-08-14) 134 | ------------------- 135 | 136 | 0.7.12 (2015-04-07) 137 | ------------------- 138 | 139 | 0.7.11 (2015-03-23) 140 | ------------------- 141 | 142 | 0.7.10 (2015-02-25) 143 | ------------------- 144 | 145 | 0.7.9 (2015-02-24) 146 | ------------------ 147 | 148 | 0.7.8 (2015-01-16) 149 | ------------------ 150 | 151 | 0.7.7 (2015-01-06) 152 | ------------------ 153 | 154 | 0.7.6 (2014-12-26) 155 | ------------------ 156 | * 0.7.5 157 | * update changelog 158 | * 0.7.4 159 | * changelog updated 160 | * 0.7.3 161 | * changelog updated 162 | * 0.7.2 163 | * changelog updated 164 | * 0.7.1 165 | * update changelog 166 | * 0.7.0 167 | * changelog updated 168 | * Contributors: Jihoon Lee, Russell Toris 169 | 170 | 0.7.5 (2014-12-26) 171 | ------------------ 172 | 173 | 0.7.4 (2014-12-16) 174 | ------------------ 175 | 176 | 0.7.3 (2014-12-15) 177 | ------------------ 178 | 179 | 0.7.2 (2014-12-15) 180 | ------------------ 181 | * 0.7.1 182 | * update changelog 183 | * Contributors: Jihoon Lee 184 | 185 | 0.7.1 (2014-12-09) 186 | ------------------ 187 | 188 | 0.7.0 (2014-12-02) 189 | ------------------ 190 | 191 | 0.6.8 (2014-11-05) 192 | ------------------ 193 | 194 | 0.6.7 (2014-10-22) 195 | ------------------ 196 | * updated package manifests 197 | * Contributors: Russell Toris 198 | 199 | 0.6.6 (2014-10-21) 200 | ------------------ 201 | 202 | 0.6.5 (2014-10-14) 203 | ------------------ 204 | * 0.6.4 205 | * update changelog 206 | * 0.6.3 207 | * update change log 208 | * Contributors: Jihoon Lee 209 | 210 | 0.6.4 (2014-10-08) 211 | ------------------ 212 | 213 | 0.6.3 (2014-10-07) 214 | ------------------ 215 | 216 | 0.6.2 (2014-10-06) 217 | ------------------ 218 | 219 | 0.6.1 (2014-09-01) 220 | ------------------ 221 | 222 | 0.6.0 (2014-05-23) 223 | ------------------ 224 | 225 | 0.5.4 (2014-04-17) 226 | ------------------ 227 | 228 | 0.5.3 (2014-03-28) 229 | ------------------ 230 | 231 | 0.5.2 (2014-03-14) 232 | ------------------ 233 | 234 | 0.5.1 (2013-10-31) 235 | ------------------ 236 | 237 | 0.5.0 (2013-07-17) 238 | ------------------ 239 | * 0.5.0 preparation for hydro release 240 | * Contributors: Jihoon Lee 241 | 242 | 0.4.4 (2013-04-08) 243 | ------------------ 244 | * adding russl and myself as maintainer. adding build_tool depend 245 | * Contributors: Jihoon Lee 246 | 247 | 0.4.3 (2013-04-03 08:24) 248 | ------------------------ 249 | * adding CMake list for meta pkg 250 | * Contributors: Jihoon Lee 251 | 252 | 0.4.2 (2013-04-03 08:12) 253 | ------------------------ 254 | 255 | 0.4.1 (2013-03-07) 256 | ------------------ 257 | 258 | 0.4.0 (2013-03-05) 259 | ------------------ 260 | * cleaning up meta package 261 | * Catkinizing rosbridge_library and server. 262 | * Collapse directory structure. 263 | * Removed print statements and also made sure to cast any tuples to lists. 264 | * Removed the pypng dependency and finalised PIL dependency 265 | * Use python imaging library to encode PNG instead of pypng 266 | * Added the ujson library, modified cmakelists to install ujson to the 267 | user python directory. 268 | * Fixed an inconsequential elif bug. 269 | * Refactored to use simplejson if the package is installed. 270 | * Added simplejson library and moved the location of the libraries. 271 | * Temporary commit adding profiling messages. something is going awry. 272 | * Renamed rosbridge stack to rosbridge_suite 273 | * Contributors: Austin Hendrix, Brandon Alexander, Jihoon Lee, jon 274 | -------------------------------------------------------------------------------- /rosbridge_suite/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rosbridge_suite NONE) 3 | find_package(ament_cmake_core REQUIRED) 4 | ament_package() 5 | -------------------------------------------------------------------------------- /rosbridge_suite/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rosbridge_suite 4 | 2.3.0 5 | 6 | Rosbridge provides a JSON API to ROS functionality for non-ROS programs. 7 | There are a variety of front ends that interface with rosbridge, including 8 | a WebSocket server for web browsers to interact with. 9 | 10 | Rosbridge_suite is a meta-package containing rosbridge, various front end 11 | packages for rosbridge like a WebSocket package, and helper packages. 12 | 13 | 14 | BSD 15 | 16 | http://ros.org/wiki/rosbridge_suite 17 | https://github.com/RobotWebTools/rosbridge_suite/issues 18 | https://github.com/RobotWebTools/rosbridge_suite 19 | 20 | Jonathan Mace 21 | Błażej Sowa 22 | 23 | ament_cmake 24 | 25 | rosbridge_library 26 | rosbridge_server 27 | rosapi 28 | 29 | 30 | ament_cmake 31 | 32 | 33 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package rosbridge_test_msgs 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.3.0 (2025-05-19) 6 | ------------------ 7 | 8 | 2.2.0 (2025-02-26) 9 | ------------------ 10 | * Update maintainers (`#1000 `_) 11 | * Contributors: Błażej Sowa 12 | 13 | 2.1.0 (2024-10-08) 14 | ------------------ 15 | 16 | 2.0.0 (2024-10-08) 17 | ------------------ 18 | * Fix action cancellation by passing status from JSON (`#953 `_) 19 | * Fix length-limited list types (`#840 `_) 20 | * Support actions in rosbridge protocol (`#886 `_) 21 | * Contributors: Ezra Brooks, Sebastian Castro 22 | 23 | 1.3.2 (2023-09-27) 24 | ------------------ 25 | 26 | 1.3.1 (2022-10-21) 27 | ------------------ 28 | 29 | 1.3.0 (2022-08-16) 30 | ------------------ 31 | 32 | 1.2.0 (2022-05-20) 33 | ------------------ 34 | 35 | 1.1.2 (2022-01-03) 36 | ------------------ 37 | 38 | 1.1.1 (2021-12-09) 39 | ------------------ 40 | 41 | 1.1.0 (2021-10-22) 42 | ------------------ 43 | * Move msg/srv from rosapi and rosbridge_library into separate packages; enable Rolling in CI (`#665 `_) 44 | * Contributors: Jacob Bandes-Storch 45 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rosbridge_test_msgs) 3 | 4 | find_package(ament_cmake_core REQUIRED) 5 | find_package(ament_cmake_python REQUIRED) 6 | find_package(builtin_interfaces REQUIRED) 7 | find_package(geometry_msgs REQUIRED) 8 | find_package(rosidl_default_generators REQUIRED) 9 | find_package(std_msgs REQUIRED) 10 | 11 | rosidl_generate_interfaces(${PROJECT_NAME} 12 | msg/Num.msg 13 | msg/TestChar.msg 14 | msg/TestDurationArray.msg 15 | msg/TestHeaderArray.msg 16 | msg/TestHeader.msg 17 | msg/TestHeaderTwo.msg 18 | msg/TestTimeArray.msg 19 | msg/TestUInt8.msg 20 | msg/TestUInt8FixedSizeArray16.msg 21 | msg/TestFloat32Array.msg 22 | msg/TestFloat32BoundedArray.msg 23 | msg/TestNestedBoundedArray.msg 24 | srv/AddTwoInts.srv 25 | srv/SendBytes.srv 26 | srv/TestArrayRequest.srv 27 | srv/TestEmpty.srv 28 | srv/TestMultipleRequestFields.srv 29 | srv/TestMultipleResponseFields.srv 30 | srv/TestNestedService.srv 31 | srv/TestRequestAndResponse.srv 32 | srv/TestRequestOnly.srv 33 | srv/TestResponseOnly.srv 34 | action/TestEmpty.action 35 | action/TestFeedbackAndResult.action 36 | action/TestGoalAndResult.action 37 | action/TestGoalFeedbackAndResult.action 38 | action/TestGoalOnly.action 39 | action/TestMultipleGoalFields.action 40 | action/TestResultOnly.action 41 | DEPENDENCIES builtin_interfaces geometry_msgs std_msgs 42 | ) 43 | 44 | ament_export_dependencies(rosidl_default_runtime) 45 | 46 | ament_package() 47 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/action/TestEmpty.action: -------------------------------------------------------------------------------- 1 | # Empty goal 2 | --- 3 | # Empty feedback 4 | --- 5 | # Empty result 6 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/action/TestFeedbackAndResult.action: -------------------------------------------------------------------------------- 1 | # Empty goal 2 | --- 3 | # Empty feedback 4 | --- 5 | # Result 6 | int32 data 7 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/action/TestGoalAndResult.action: -------------------------------------------------------------------------------- 1 | # Goal 2 | int32 data 3 | --- 4 | # Empty feedback 5 | --- 6 | # result 7 | int32 data 8 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/action/TestGoalFeedbackAndResult.action: -------------------------------------------------------------------------------- 1 | # Goal 2 | int32 data 3 | --- 4 | # Feedback 5 | int32 data 6 | --- 7 | # result 8 | int32 data 9 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/action/TestGoalOnly.action: -------------------------------------------------------------------------------- 1 | # Goal 2 | int32 data 3 | --- 4 | # Empty feedback 5 | --- 6 | # Empty result 7 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/action/TestMultipleGoalFields.action: -------------------------------------------------------------------------------- 1 | # Goal 2 | int32 int_value 3 | float32 float_value 4 | string string 5 | bool bool_value 6 | --- 7 | # Empty feedback 8 | --- 9 | # Empty result 10 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/action/TestResultOnly.action: -------------------------------------------------------------------------------- 1 | # Empty goal 2 | --- 3 | # Empty feedback 4 | --- 5 | # Result 6 | int32 data 7 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/Num.msg: -------------------------------------------------------------------------------- 1 | int64 num 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestChar.msg: -------------------------------------------------------------------------------- 1 | char[] data 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestDurationArray.msg: -------------------------------------------------------------------------------- 1 | builtin_interfaces/Duration[] durations 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestFloat32Array.msg: -------------------------------------------------------------------------------- 1 | float32[] data 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestFloat32BoundedArray.msg: -------------------------------------------------------------------------------- 1 | float32[16] data 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestHeader.msg: -------------------------------------------------------------------------------- 1 | std_msgs/Header header 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestHeaderArray.msg: -------------------------------------------------------------------------------- 1 | std_msgs/Header[] header 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestHeaderTwo.msg: -------------------------------------------------------------------------------- 1 | std_msgs/Header header 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestNestedBoundedArray.msg: -------------------------------------------------------------------------------- 1 | TestFloat32BoundedArray data 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestTimeArray.msg: -------------------------------------------------------------------------------- 1 | builtin_interfaces/Time[] times 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestUInt8.msg: -------------------------------------------------------------------------------- 1 | uint8[] data 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/msg/TestUInt8FixedSizeArray16.msg: -------------------------------------------------------------------------------- 1 | uint8[16] data 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rosbridge_test_msgs 4 | 2.3.0 5 | 6 | Message and service definitions used in internal tests for rosbridge packages. 7 | 8 | 9 | BSD 10 | 11 | http://ros.org/wiki/rosbridge_library 12 | https://github.com/RobotWebTools/rosbridge_suite/issues 13 | https://github.com/RobotWebTools/rosbridge_suite 14 | 15 | Jonathan Mace 16 | Błażej Sowa 17 | 18 | ament_cmake 19 | 20 | builtin_interfaces 21 | std_msgs 22 | geometry_msgs 23 | rosidl_default_generators 24 | 25 | builtin_interfaces 26 | rclpy 27 | std_msgs 28 | geometry_msgs 29 | rosidl_default_runtime 30 | 31 | action_msgs 32 | ament_cmake_pytest 33 | builtin_interfaces 34 | diagnostic_msgs 35 | example_interfaces 36 | geometry_msgs 37 | nav_msgs 38 | sensor_msgs 39 | std_msgs 40 | std_srvs 41 | stereo_msgs 42 | tf2_msgs 43 | trajectory_msgs 44 | visualization_msgs 45 | 46 | rosidl_interface_packages 47 | 48 | 49 | ament_cmake 50 | 51 | 52 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/AddTwoInts.srv: -------------------------------------------------------------------------------- 1 | int64 a 2 | int64 b 3 | --- 4 | int64 sum 5 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/SendBytes.srv: -------------------------------------------------------------------------------- 1 | int64 count 2 | --- 3 | string data 4 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestArrayRequest.srv: -------------------------------------------------------------------------------- 1 | int32[] int_values 2 | --- 3 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestEmpty.srv: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestMultipleRequestFields.srv: -------------------------------------------------------------------------------- 1 | int32 int_value 2 | float32 float_value 3 | string string 4 | bool bool_value 5 | --- 6 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestMultipleResponseFields.srv: -------------------------------------------------------------------------------- 1 | --- 2 | int32 int_value 3 | float32 float_value 4 | string string 5 | bool bool_value 6 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestNestedService.srv: -------------------------------------------------------------------------------- 1 | #request definition 2 | geometry_msgs/Pose pose 3 | --- 4 | #response definition 5 | std_msgs/Float64 data 6 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestRequestAndResponse.srv: -------------------------------------------------------------------------------- 1 | int32 data 2 | --- 3 | int32 data 4 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestRequestOnly.srv: -------------------------------------------------------------------------------- 1 | int32 data 2 | --- 3 | -------------------------------------------------------------------------------- /rosbridge_test_msgs/srv/TestResponseOnly.srv: -------------------------------------------------------------------------------- 1 | --- 2 | int32 data 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [codespell] 2 | ignore-words-list = miror 3 | 4 | [flake8] 5 | count = True 6 | max-complexity = 36 7 | max-line-length = 217 8 | ignore = E203,W503 9 | show-source = True 10 | statistics = True 11 | 12 | [isort] 13 | profile = black 14 | --------------------------------------------------------------------------------