diff --git a/configs/black.toml b/.configs/black.toml similarity index 100% rename from configs/black.toml rename to .configs/black.toml diff --git a/configs/mypy.ini b/.configs/mypy.ini similarity index 77% rename from configs/mypy.ini rename to .configs/mypy.ini index 403c404..9d2cd3c 100644 --- a/configs/mypy.ini +++ b/.configs/mypy.ini @@ -10,6 +10,6 @@ warn_unreachable = True warn_redundant_casts = True no_implicit_optional = True files = - bdai_ros2_wrappers/bdai_ros2_wrappers, - bdai_ros2_wrappers/test -exclude = "^(docker|.*external|.*thirdparty|.*install|.*build|.*_experimental|.*conversions.py)/" + synchros2/synchros2, + synchros2/test +exclude = "^(docker|.*external|.*thirdparty|.*install|.*build|.*_experimental)/" diff --git a/configs/ruff.toml b/.configs/ruff.toml similarity index 95% rename from configs/ruff.toml rename to .configs/ruff.toml index 9ac2278..101ca2f 100644 --- a/configs/ruff.toml +++ b/.configs/ruff.toml @@ -47,9 +47,7 @@ target-version = "py38" # We don't require docstrings in tests "**/conftest.py" = ["D"] "**/test_*.py" = ["D"] -"proto2ros_tests/*" = ["D"] - -"bdai_ros2_wrappers/examples/*" = ["D"] +"**/examples/*" = ["D"] [mccabe] # Unlike Flake8, default to a complexity level of 10. diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 5b18f99..77c0e47 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,22 +1,11 @@ -FROM ros:humble-ros-base-jammy +ARG ROS_DISTRO=humble +FROM ros:${ROS_DISTRO}-ros-base +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV ROS_DISTRO=${ROS_DISTRO} SHELL ["/bin/bash", "-c"] -# setup environment -ENV LANG C.UTF-8 -ENV LC_ALL C.UTF-8 - -# install packages -RUN apt-get update \ - && apt-get install -q -y --no-install-recommends \ - curl \ - && rm -rf /var/lib/apt/lists/* - -ARG ROS_DISTRO=humble -ENV ROS_DISTRO $ROS_DISTRO -ARG INSTALL_PACKAGE=base - -SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN DEBIAN_FRONTEND=noninteractive apt-get update -q && \ apt-get update -q && \ apt-get install -yq --no-install-recommends \ @@ -27,31 +16,14 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update -q && \ python3-colcon-mixin \ python3-pytest-cov \ python3-rosdep \ - libpython3-dev \ python3-vcstool && \ rm -rf /var/lib/apt/lists/* -# I added this RUN source "/opt/ros/${ROS_DISTRO}/setup.bash" -# Ros Example Packages -RUN apt-get update -q && \ - apt-get install -y --no-install-recommends \ - ros-${ROS_DISTRO}-action-tutorials-interfaces \ - ros-${ROS_DISTRO}-example-interfaces && \ - rm -rf /var/lib/apt/lists/* - -# Install packages inside the new environment -RUN python -m pip install --no-cache-dir --upgrade pip==22.3.1 \ - && pip install --root-user-action=ignore --no-cache-dir --default-timeout=900 \ - numpy==1.24.1 \ - && pip cache purge - -# Install dependencies for repository packages RUN --mount=type=bind,source=.,target=/tmp/context \ apt-get update -q && rosdep update && \ rosdep install -y -i --from-paths /tmp/context && \ rm -rf /var/lib/apt/lists/* -# ROS doesn't recognize the docker shells as terminals so force colored output ENV RCUTILS_COLORIZED_OUTPUT=1 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..fe336e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve. +title: '' +labels: bug +--- + +## Bug description + +**Platform**: +- OS [e.g. Ubuntu Jammy]: +- ROS [e.g. Humble]: +- `proto2ros` version [e.g. ag, commit sha]: + +### How to reproduce + + + +**Expected behavior**: + +**Actual behavior**: + +## Additional context + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..48630ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for a new feature. +title: '' +labels: enhancement +--- + +## Feature description + + + +## Additional considerations + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..df52c87 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Proposed changes + + + +### Checklist + + + +- [ ] Lint and unit tests pass locally +- [ ] I have added tests that prove my changes are effective +- [ ] I have added necessary documentation to communicate the changes + +### Additional comments + + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d79c8a..a403bfc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,13 +5,13 @@ repos: rev: 'v0.0.263' hooks: - id: ruff - args: ['--fix', '--config', 'configs/ruff.toml'] + args: ['--fix', '--config', '.configs/ruff.toml'] - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black language_version: python3.10 - args: ['--config', 'configs/black.toml'] + args: ['--config', '.configs/black.toml'] verbose: true - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 @@ -42,7 +42,7 @@ repos: rev: v1.2.0 hooks: - id: mypy - args: ['--config-file', 'configs/mypy.ini'] + args: ['--config-file', '.configs/mypy.ini'] pass_filenames: false additional_dependencies: - types-protobuf diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7bb1ca0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ + +# Contributing to `ros_utilities` + +First off, thanks for taking the time to contribute! ❤️ + +All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 + +> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: +> - Star the project +> - Tweet about it +> - Refer this project in your project's readme +> - Mention the project at local meetups and tell your friends/colleagues + + +## Table of Contents + +- [I Have a Question](#i-have-a-question) +- [I Want To Contribute](#i-want-to-contribute) + - [Reporting Bugs](#reporting-bugs) + - [Requesting Features](#requesting-features) + - [Your First Code Contribution](#your-first-code-contribution) + - [Improving The Documentation](#improving-the-documentation) + +## I Have a Question + +Before you ask a question, it is best to search for existing [issues](https://github.com/bdaiinstitute/ros_utilities/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. + +If you then still feel the need to ask a question and need clarification, we recommend the following: + +- Open an [issue](https://github.com/utilities/ros_utilities/issues/new). +- Provide as much context as you can about what you're running into. +- Provide project and platform versions, depending on what seems relevant. + +We will then take care of the issue as soon as possible. + +## I Want To Contribute + +> [!IMPORTANT] +> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content, and that the content you contribute may be provided under the project license. + +### Reporting Bugs + + +#### Before Submitting a Bug Report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. + +- Make sure that you are using the latest version. +- Determine if your bug is really a bug and not an error on your side. If you are looking for support, you might want to check [this section](#i-have-a-question)). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/bdaiinstitute/ros_utilities/issues?q=label%3Abug). +- Collect information about the bug: + - Stack trace (Traceback) + - OS, ROS, Platform and Version (Windows, Linux, macOS, x86, ARM) + - Package versions, package manager, depending on what seems relevant. + - Possibly your input and the output + - Can you reliably reproduce the issue? And can you also reproduce it with older versions? + + +#### How Do I Submit a Good Bug Report? + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [issue](https://github.com/bdaiinstitute/ros_utilities/issues/new). +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. +- Provide the information you collected in the previous section. + +Once it's filed: + +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided steps. +- If the team is able to reproduce the issue, it will be confirmed as a `bug` and the issue will be left to be [addressed by someone](#your-first-code-contribution). + +### Requesting Features + + +#### Before Submitting a Feature Request + +- Make sure that you are using the latest version. +- Read the documentation carefully and ensure the functionality is indeed missing. +- Perform a [search](https://github.com/bdaiinstitute/ros_utilities/issues) to see if the feature has already been requested. If it has, add a comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. + + +#### How Do I Submit a Good Feature Request? + +Feature requests are tracked as [GitHub issues](https://github.com/bdaiinstitute/ros_utilities/issues). + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. +- **Explain why this enhancement would be useful** to most `ros_utilities` users. You may also want to point out the other projects that solved it better and which could serve as inspiration. + + + +### Your First Code Contribution + +- Install a suitable [ROS distribution](https://docs.ros.org/en/humble/Installation.html). + +- [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) on GitHub. + +- Clone your repository fork under a local ROS workspace: + + ```sh + mkdir -p path/to/workspace/src + cd path/to/workspace/src + git clone https://github.com/user-name/ros_utilities.git + cd - + ``` + +- Install `pre-commit` hooks for it: + + ```sh + cd path/to/workspace/src/ros_utilities + pip install pre-commit + pre-commit install + cd - + ``` + +- Make the intended changes with appropriate tests that validate it. +- Push these changes and open a pull request against this repository. + + +## Attribution +This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! diff --git a/README.md b/README.md index 405c9c0..507125f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ -# ros_utilities -Wrappers and other utilities for ROS2 - -## Usage - -See the [`ros_utilities` wiki](https://github.com/bdaiinstitute/ros_utilities/wiki) on how to make the best out of these utilities. - -## Contribution -To contribute, install `pre-commit` via pip, run `pre-commit install` and then run `pre-commit run --all-files` to -verify that your code will pass inspection. -```bash -git clone https://github.com/bdaiinstitute/ros_utilities.git -cd ros_utilities -pip3 install pre-commit -pre-commit install -pre-commit run --all-files -``` - -Now whenever you commit code to this repository, it will be checked against our `pre-commit` hooks. You can also run -`git commit --no-verify` if you wish you commit without checking against the hooks. +# `ros_utilities` + +![Python Support](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10-blue) +![ROS Support](https://img.shields.io/badge/ROS-humble-blue) + +## Overview + +`ros_utilities` enable a different, at times simpler approach to ROS 2 programming, particularly for those that come with a ROS 1 background. + +## Packages + +This repository contains the following packages: + +| Package | Description | +|-------------------------------------| -----------------------------------------------------------------------------------| +| [`synchros2`](synchros2) | `rclpy` wrappers to ease idiomatic, synchronous ROS 2 programming in Python. | + +**Note**: `proto2ros` packages for Protobuf / ROS 2 interoperability used to live in this repository but now live in https://github.com/bdaiinstitute/proto2ros. + +## Next steps + +See [contribution guidelines](CONTRIBUTING.md)! diff --git a/bdai_ros2_wrappers/LICENSE b/bdai_ros2_wrappers/LICENSE deleted file mode 100644 index 30e8e2e..0000000 --- a/bdai_ros2_wrappers/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/__init__.py b/bdai_ros2_wrappers/bdai_ros2_wrappers/__init__.py index 6d61717..dd73d13 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/__init__.py +++ b/bdai_ros2_wrappers/bdai_ros2_wrappers/__init__.py @@ -1 +1,5 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute Inc. All rights reserved. +# Copyright (c) 2024 Boston Dynamics AI Institute Inc. All rights reserved. + +import sys + +sys.modules[__name__] = __import__("synchros2") diff --git a/bdai_ros2_wrappers/package.xml b/bdai_ros2_wrappers/package.xml index cfd73e6..781f4ed 100644 --- a/bdai_ros2_wrappers/package.xml +++ b/bdai_ros2_wrappers/package.xml @@ -2,17 +2,12 @@ bdai_ros2_wrappers - 0.0.1 - The AI Institute's wrappers for ROS2 - The AI Institute + 1.0.0 + Former AI Institute's wrappers for ROS 2, an alias for synchros2 + The AI Institute MIT - action_msgs - rclpy - - example_interfaces - python3-typing-extensions - python3-pytest + synchros2 ament_python diff --git a/bdai_ros2_wrappers/setup.py b/bdai_ros2_wrappers/setup.py index cbb36e9..a144595 100644 --- a/bdai_ros2_wrappers/setup.py +++ b/bdai_ros2_wrappers/setup.py @@ -6,21 +6,16 @@ setup( name=package_name, - version="0.0.1", + version="1.0.0", packages=[package_name], - package_data={package_name: ["py.typed"]}, data_files=[ ("share/ament_index/resource_index/packages", ["resource/" + package_name]), ("share/" + package_name, ["package.xml"]), ], install_requires=["setuptools"], - zip_safe=True, maintainer="The AI Institute", maintainer_email="engineering@theaiinstitute.com", description="The AI Institute's wrappers for ROS2", + zip_safe=True, license="MIT", - tests_require=["pytest"], - entry_points={ - "console_scripts": [], - }, ) diff --git a/proto2ros/CMakeLists.txt b/proto2ros/CMakeLists.txt deleted file mode 100644 index 98079d2..0000000 --- a/proto2ros/CMakeLists.txt +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. -cmake_minimum_required(VERSION 3.9) -if(POLICY CMP0148) - cmake_policy(SET CMP0148 OLD) # to accommodate rosidl pipeline -endif() -project(proto2ros) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -find_program(CCACHE_PROGRAM ccache) -if(CCACHE_PROGRAM) - set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") -endif() - -set(DEFAULT_BUILD_TYPE "Release") -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message(STATUS "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.") - set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of the build." FORCE) - # Set the possible values of the build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") -endif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - -if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") - include(CheckIPOSupported) - check_ipo_supported(RESULT ipo_supported OUTPUT output) - if(ipo_supported) - set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) - message(STATUS "We are in release - Successfully enabled IPO") - else() - message(WARNING "IPO not supported - Skipping reason: ${output}") - endif() -endif() - -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# find dependencies -find_package(ament_cmake REQUIRED) -find_package(ament_cmake_python REQUIRED) -find_package(rosidl_default_generators REQUIRED) -find_package(builtin_interfaces REQUIRED) -find_package(std_msgs REQUIRED) -find_package(rclcpp REQUIRED) - -find_package(Protobuf REQUIRED) - -rosidl_generate_interfaces(${PROJECT_NAME} - msg/Any.msg msg/AnyProto.msg msg/Bytes.msg msg/List.msg - msg/Struct.msg msg/StructEntry.msg msg/Value.msg -) - -add_library(${PROJECT_NAME}_conversions SHARED src/conversions.cpp) -set_target_properties(${PROJECT_NAME}_conversions PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) -target_compile_options(${PROJECT_NAME}_conversions PUBLIC "$<$:-O0>") -target_compile_definitions(${PROJECT_NAME}_conversions - PUBLIC - "$<$:DEBUG>" - "$<$,$>:NDEBUG>" -) -target_include_directories(${PROJECT_NAME}_conversions PUBLIC - "$" - "$" -) -rosidl_get_typesupport_target(${PROJECT_NAME}_cpp_msgs ${PROJECT_NAME} "rosidl_typesupport_cpp") -target_link_libraries(${PROJECT_NAME}_conversions ${${PROJECT_NAME}_cpp_msgs} protobuf::libprotobuf) -ament_target_dependencies(${PROJECT_NAME}_conversions builtin_interfaces rclcpp std_msgs) - -find_program(CLANG_TIDY_EXECUTABLE NAMES "clang-tidy" REQUIRED) -set(CXX_CLANG_TIDY "${CLANG_TIDY_EXECUTABLE}" - "-header-filter=.*proto2ros/.*conversions.hpp" - "-checks=-clang-diagnostic-ignored-optimization-argument") -set_target_properties(${PROJECT_NAME}_conversions PROPERTIES CXX_CLANG_TIDY "${CXX_CLANG_TIDY}") - -include(cmake/rosidl_helpers.cmake) -rosidl_generated_python_package_add( - ${PROJECT_NAME}_additional_modules - PACKAGES ${PROJECT_NAME} - DESTINATION ${PROJECT_NAME} -) - -install( - DIRECTORY include/ - DESTINATION include/${PROJECT_NAME} -) - -install( - TARGETS ${PROJECT_NAME}_conversions - EXPORT ${PROJECT_NAME} - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin -) -ament_export_targets(${PROJECT_NAME}) - -install( - DIRECTORY cmake - DESTINATION share/${PROJECT_NAME} -) - -ament_export_dependencies(builtin_interfaces) -ament_export_dependencies(rosidl_default_runtime) -ament_export_dependencies(std_msgs) - -ament_package(CONFIG_EXTRAS "proto2ros-extras.cmake") diff --git a/proto2ros/LICENSE b/proto2ros/LICENSE deleted file mode 100644 index aa696ca..0000000 --- a/proto2ros/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Boston Dynamics AI Institute - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/proto2ros/README.md b/proto2ros/README.md deleted file mode 100644 index 1dfa93a..0000000 --- a/proto2ros/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Protobuf to ROS 2 interoperability - -`proto2ros` helps maintain an interoperability layer between Protobuf dependent and ROS 2 aware code by generating equivalent ROS 2 message definitions given source Protobuf message definitions, as well bi-directional conversion APIs in relevant languages (such as Python). To date, Protobuf syntax versions 2 and 3 are supported but only syntax version 3 has been extensively tested. - -Continue on to the [`ros_utilities` wiki](https://github.com/bdaiinstitute/ros_utilities/wiki) for further reference. diff --git a/proto2ros/cmake/proto2ros_generate.cmake b/proto2ros/cmake/proto2ros_generate.cmake deleted file mode 100644 index 9dd24da..0000000 --- a/proto2ros/cmake/proto2ros_generate.cmake +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# Generates Protobuf <-> ROS 2 interoperability interfaces. -# -# :param target: the target name for the generation process, so it can be depended on. -# :param PACKAGE_NAME: name of the package that will host the generated interfaces. -# Defaults to the current project name. -# :param PROTOS: Protobuf message files to generate interoperability interfaces for. -# These are compiled to a single Protobuf descriptor file for further processing. -# :param IMPORT_DIRS: optional import paths to pass to protoc. Only used when PROTOS -# are provided. If none is given, parent directories of PROTOS are used instead. -# :param PROTO_DESCRIPTORS: Protobuf descriptor files for the Protobuf messages -# that the generation process will produce interoperability interfaces for. -# :param CONFIG_FILE: optional base configuration file for the generation process. -# :param CONFIG_OVERLAYS: optional configuration file overlays to be applied sequentially -# over the (provided or default) base configuration file. -# :param INTERFACES_OUT_VAR: the name of the variable to yield ROS 2 interface tuples. -# Defaults to the target name with an `_interfaces` suffix. -# :param PYTHON_OUT_VAR: the name of the variable to yield generated Python sources. -# Defaults to the target name with a `_python_sources` suffix. -# :param CPP_OUT_VAR: the name of the variable to yield generated C++ sources -# (both .cpp and .hpp files). Defaults to the target name with a `_cpp_sources` suffix. -# :param INCLUDE_OUT_VAR: the name of the variable to yield the path to generated -# C++ includes. Defaults to the target name with a `_cpp_include` suffix. -# :param DEPENDS: optional, additional dependencies to the generation command. -# This can be useful to depend on earlier protobuf_generate() commands. -# :param APPEND_PYTHONPATH: optional paths to append to the PYTHONPATH that applies -# to the generation process. -# :param NO_LINT: if provided, no lint tests are added for generated code. -function(proto2ros_generate target) - cmake_parse_arguments( - ARG "NO_LINT" - "PACKAGE_NAME;CONFIG_FILE;INTERFACES_OUT_VAR;PYTHON_OUT_VAR;CPP_OUT_VAR;INCLUDE_OUT_VAR" - "PROTOS;PROTO_DESCRIPTORS;IMPORT_DIRS;CONFIG_OVERLAYS;APPEND_PYTHONPATH;DEPENDS" ${ARGN}) - if(NOT ARG_PACKAGE_NAME) - set(ARG_PACKAGE_NAME ${PROJECT_NAME}) - endif() - if(NOT ARG_INTERFACES_OUT_VAR) - set(ARG_INTERFACES_OUT_VAR ${target}_interfaces) - endif() - if(NOT ARG_PYTHON_OUT_VAR) - set(ARG_PYTHON_OUT_VAR ${target}_python_sources) - endif() - if(NOT ARG_CPP_OUT_VAR) - set(ARG_CPP_OUT_VAR ${target}_cpp_sources) - endif() - if(NOT ARG_INCLUDE_OUT_VAR) - set(ARG_INCLUDE_OUT_VAR ${target}_cpp_include) - endif() - list(APPEND ARG_APPEND_PYTHONPATH "${PROJECT_SOURCE_DIR}") - string(REPLACE ";" ":" APPEND_PYTHONPATH "${ARG_APPEND_PYTHONPATH}") - - set(BASE_PATH "${CMAKE_CURRENT_BINARY_DIR}/proto2ros_generate") - set(OUTPUT_PATH "${BASE_PATH}/${ARG_PACKAGE_NAME}") - file(REMOVE_RECURSE "${OUTPUT_PATH}") - file(MAKE_DIRECTORY "${OUTPUT_PATH}") - - foreach(path ${ARG_PROTO_DESCRIPTORS}) - get_filename_component(path "${path}" ABSOLUTE) - list(APPEND PROTO_DESCRIPTORS ${path}) - endforeach() - - if(ARG_PROTOS) - set(protoc_options --include_source_info) - set(proto_descriptor "${CMAKE_CURRENT_BINARY_DIR}/${target}.desc") - foreach(proto ${ARG_PROTOS}) - get_filename_component(proto_path "${proto}" ABSOLUTE) - if(IS_DIRECTORY "${proto_path}") - file(GLOB_RECURSE nested_files "${proto_path}" *.proto) - if(NOT ARG_IMPORT_DIRS) - list(APPEND protoc_options "-I${proto_path}") - endif() - list(APPEND proto_files ${nested_files}) - else() - get_filename_component(proto_dir "${proto_path}" DIRECTORY) - if(NOT ARG_IMPORT_DIRS) - list(APPEND protoc_options "-I${proto_dir}") - endif() - list(APPEND proto_files "${proto_path}") - endif() - endforeach() - foreach(path ${ARG_IMPORT_DIRS}) - get_filename_component(path "${path}" ABSOLUTE) - list(APPEND protoc_options "-I${path}") - endforeach() - list(REMOVE_DUPLICATES protoc_options) - - # Generate the implicit descriptor file at configuration time - # so that configuration code below can run. - get_executable_path(PROTOC_EXECUTABLE protobuf::protoc CONFIGURE) - execute_process( - COMMAND - ${PROTOC_EXECUTABLE} ${protoc_options} -o${proto_descriptor} ${proto_files} - COMMAND_ERROR_IS_FATAL ANY - ) - - add_custom_command( - OUTPUT ${proto_descriptor} - COMMAND ${PROTOC_EXECUTABLE} ${protoc_options} -o${proto_descriptor} ${proto_files} - DEPENDS ${proto_files} - COMMENT "Compile descriptor from .proto files" - VERBATIM - ) - list(APPEND PROTO_DESCRIPTORS ${proto_descriptor}) - endif() - - if(NOT PROTO_DESCRIPTORS) - message(FATAL_ERROR "No Protobuf descriptors to process") - endif() - - if(ARG_CONFIG_FILE) - get_filename_component(ARG_CONFIG_FILE "${ARG_CONFIG_FILE}" ABSOLUTE) - list(APPEND PROTO2ROS_GENERATE_OPTIONS "-c" "${ARG_CONFIG_FILE}") - endif() - foreach(overlay ${ARG_CONFIG_OVERLAYS}) - get_filename_component(overlay "${overlay}" ABSOLUTE) - list(APPEND PROTO2ROS_GENERATE_OPTIONS "-a" "${overlay}") - endforeach() - list(APPEND PROTO2ROS_GENERATE_OPTIONS "-m" "${OUTPUT_PATH}/manifest.txt") - list(APPEND PROTO2ROS_GENERATE_OPTIONS "-O" "${OUTPUT_PATH}") - - # As we cannot deduce what files will result from the generation process ahead of time, - # we perform a dry run at configuration time to determine these files. Build will be - # forced to fail until reconfiguration whenever the set of output files changes. Note - # that messages are also generated, as the rosidl pipeline requires them to exist at - # configuration time. - get_executable_path(PYTHON_EXECUTABLE Python3::Interpreter CONFIGURE) - execute_process( - COMMAND - ${CMAKE_COMMAND} -E env - "PYTHONPATH=${APPEND_PYTHONPATH}:$ENV{PYTHONPATH}" - "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python" - ${PYTHON_EXECUTABLE} -m proto2ros.cli.generate --dry --force-message-gen - ${PROTO2ROS_GENERATE_OPTIONS} ${ARG_PACKAGE_NAME} ${PROTO_DESCRIPTORS} - COMMAND_ERROR_IS_FATAL ANY - ) - file(STRINGS "${OUTPUT_PATH}/manifest.txt" output_files) - file(RENAME "${OUTPUT_PATH}/manifest.txt" "${OUTPUT_PATH}/manifest.orig.txt") - - add_custom_command( - OUTPUT ${output_files} - COMMAND - ${CMAKE_COMMAND} -E env "PYTHONPATH=${APPEND_PYTHONPATH}:$ENV{PYTHONPATH}" "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python" - ${PYTHON_EXECUTABLE} -m proto2ros.cli.generate ${PROTO2ROS_GENERATE_OPTIONS} ${ARG_PACKAGE_NAME} ${PROTO_DESCRIPTORS} - COMMAND ${CMAKE_COMMAND} -E compare_files "${OUTPUT_PATH}/manifest.txt" "${OUTPUT_PATH}/manifest.orig.txt" - COMMENT "Generate Protobuf <-> ROS interop interfaces (must reconfigure if the cardinality of the output set changes)" - DEPENDS ${PROTO_DESCRIPTORS} ${ARG_DEPENDS} - VERBATIM - ) - add_custom_target(${target} DEPENDS ${output_files}) - - set(interface_files ${output_files}) - list(FILTER interface_files INCLUDE REGEX ".*\.msg$") - string(REPLACE "${OUTPUT_PATH}/" "${OUTPUT_PATH}:" interface_tuples "${interface_files}") - set(python_sources ${output_files}) - list(FILTER python_sources INCLUDE REGEX ".*\.py$") - set(cpp_sources ${output_files}) - list(FILTER cpp_sources INCLUDE REGEX ".*\.[ch]pp$") - - if(BUILD_TESTING AND NOT ARG_NO_LINT AND ament_cmake_mypy_FOUND) - set(MYPY_PATH "${APPEND_PYTHONPATH}:$ENV{PYTHONPATH}") - configure_file( - "${proto2ros_DIR}/templates/mypy.ini.in" - "${BASE_PATH}/mypy.ini" @ONLY - ) - ament_mypy(${python_sources} - TESTNAME ${target}_mypy - CONFIG_FILE "${BASE_PATH}/mypy.ini" - ) - endif() - set(${ARG_INTERFACES_OUT_VAR} ${interface_tuples} PARENT_SCOPE) - set(${ARG_PYTHON_OUT_VAR} ${python_sources} PARENT_SCOPE) - set(${ARG_CPP_OUT_VAR} ${cpp_sources} PARENT_SCOPE) - set(${ARG_INCLUDE_OUT_VAR} ${BASE_PATH} PARENT_SCOPE) -endfunction() diff --git a/proto2ros/cmake/proto2ros_vendor_package.cmake b/proto2ros/cmake/proto2ros_vendor_package.cmake deleted file mode 100644 index 31e10f9..0000000 --- a/proto2ros/cmake/proto2ros_vendor_package.cmake +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# Vendors a package providing Protobuf messages, adding ROS 2 interoperability interfaces. -# -# :param target: the target name for the process, so it can be depended on. -# :param PACKAGE_NAME: name of the package that will host the generated interfaces. -# Defaults to the current project name. -# :param PROTOS: Protobuf message files to generate interoperability interfaces for. -# These are compiled to a single Protobuf descriptor file for further processing. -# :param IMPORT_DIRS: optional import paths to pass to protoc. Only used when PROTOS -# are provided. If none is given, parent directories of PROTOS are used instead. -# :param CONFIG_OVERLAYS: optional configuration file overlays to be applied sequentially -# over the default base configuration file. -# :param ROS_DEPENDENCIES: optional ROS package name to depend on for message generation and builds. -# :param CPP_DEPENDENCIES: optional C++ targets to depend on for library builds. -# :param CPP_INCLUDES: optional, additional C++ includes to use when building C++ sources. -# If none is provided and ${CMAKE_CURRENT_SOURCE_DIR}/include/${ARG_PACKAGE_NAME} exists -# as a directory, then it will be picked up by default. -# :param CPP_SOURCES: optional, additional C++ sources to build alongside generated C++ sources. -# If none is provided and both ${CMAKE_CURRENT_SOURCE_DIR}/include/${ARG_PACKAGE_NAME} and -# ${CMAKE_CURRENT_SOURCE_DIR}/src exist as directories, then source files matching either -# ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc or ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp glob -# expressions will be picked up by default. -# :param PYTHON_MODULES: optional, additional Python module sources to install -# alongside generated Python modules. -# :param PYTHON_PACKAGES: optional, additional Python packages to install alongside -# generated Python packages. If none is provided and one is found under the -# ${CMAKE_CURRENT_SOURCE_DIR}/${PACKAGE_NAME}, it will be picked up by default. -# :param DEPENDS: optional, additional dependencies to the generation command. -# This can be useful to depend on earlier protobuf_generate() commands. -# :param NO_LINT: if provided, no lint tests are added for generated code. -macro(proto2ros_vendor_package target) - set(options NO_LINT) - set(one_value_keywords PACKAGE_NAME) - set( - multi_value_keywords - PROTOS IMPORT_DIRS CONFIG_OVERLAYS ROS_DEPENDENCIES CPP_DEPENDENCIES - CPP_INCLUDES CPP_SOURCES PYTHON_MODULES PYTHON_PACKAGES DEPENDS - ) - cmake_parse_arguments(ARG "${options}" "${one_value_keywords}" "${multi_value_keywords}" ${ARGN}) - - if(NOT ARG_PACKAGE_NAME) - set(ARG_PACKAGE_NAME ${PROJECT_NAME}) - endif() - - get_filename_component(package_path "${ARG_PACKAGE_NAME}" ABSOLUTE) - if(EXISTS "${package_path}/__init__.py") - list(APPEND ARG_PYTHON_PACKAGES ${ARG_PACKAGE_NAME}) - endif() - - if(NOT ARG_CPP_INCLUDES) - if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/${ARG_PACKAGE_NAME}") - list(APPEND ARG_CPP_INCLUDES "${CMAKE_CURRENT_SOURCE_DIR}/include") - endif() - endif() - - if(NOT ARG_CPP_SOURCES) - if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/${ARG_PACKAGE_NAME}" AND IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src") - file(GLOB ARG_CPP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc") - endif() - endif() - - set(proto2ros_generate_OPTIONS) - if(ARG_NO_LINT) - list(APPEND proto2ros_generate_OPTIONS NO_LINT) - endif() - if(ARG_DEPENDS) - list(APPEND proto2ros_generate_OPTIONS DEPENDS ${ARG_DEPENDS}) - endif() - - proto2ros_generate( - ${target}_messages_gen - PROTOS ${ARG_PROTOS} - IMPORT_DIRS ${ARG_IMPORT_DIRS} - PACKAGE_NAME ${ARG_PACKAGE_NAME} - CONFIG_OVERLAYS ${ARG_CONFIG_OVERLAYS} - INTERFACES_OUT_VAR ros_messages - PYTHON_OUT_VAR py_sources - CPP_OUT_VAR generated_cpp_sources - INCLUDE_OUT_VAR generated_cpp_include_dir - ${proto2ros_generate_OPTIONS} - ) - - rosidl_generate_interfaces( - ${target} ${ros_messages} - DEPENDENCIES ${ARG_ROS_DEPENDENCIES} builtin_interfaces proto2ros - ) - add_dependencies(${target} ${target}_messages_gen) - - rosidl_generated_python_package_add( - ${target}_additional_modules - MODULES ${ARG_PYTHON_MODULES} ${py_sources} - PACKAGES ${ARG_PYTHON_PACKAGES} - DESTINATION ${target} - ) - - add_library(${target}_conversions SHARED ${generated_cpp_sources} ${ARG_CPP_SOURCES}) - target_compile_features(${target}_conversions PRIVATE cxx_std_17) - # NOTE: conversion APIs cannot ignore deprecated fields, so deprecation warnings must be disabled - target_compile_options(${target}_conversions PRIVATE -Wno-deprecated -Wno-deprecated-declarations) - list(APPEND build_include_directories "$") - foreach(cpp_include_dir ${ARG_CPP_INCLUDES}) - list(APPEND build_include_directories "$") - endforeach() - target_include_directories(${target}_conversions PUBLIC - ${build_include_directories} "$" - ) - rosidl_get_typesupport_target(cpp_interfaces ${target} "rosidl_typesupport_cpp") - target_link_libraries(${target}_conversions ${cpp_interfaces} ${ARG_CPP_DEPENDENCIES}) - ament_target_dependencies(${target}_conversions - ${ARG_ROS_DEPENDENCIES} builtin_interfaces proto2ros rclcpp) - - find_program(CLANG_TIDY_EXECUTABLE NAMES "clang-tidy") - if(BUILD_TESTING AND NOT ARG_NO_LINT AND CLANG_TIDY_EXECUTABLE) - list(APPEND clang_tidy_header_regexes "${generated_cpp_include_dir}/.*hpp") - foreach(cpp_include_dir ARG_CPP_INCLUDES) - list(APPEND clang_tidy_header_regexes "${cpp_include_dir}/.*hpp") - endforeach() - list(JOIN clang_tidy_header_regexes "|" clang_tidy_header_filter) - set(CXX_CLANG_TIDY "${CLANG_TIDY_EXECUTABLE}" - "-header-filter='^(${clang_tidy_header_filter})$'" - "-checks=-clang-diagnostic-ignored-optimization-argument") - set_target_properties(${target}_conversions PROPERTIES - CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_CLANG_TIDY "${CXX_CLANG_TIDY}") - endif() - - set(generated_header_files ${generated_cpp_sources}) - list(FILTER generated_header_files INCLUDE REGEX ".*\.hpp$") - install( - FILES ${generated_header_files} - DESTINATION include/${PROJECT_NAME}/${ARG_PACKAGE_NAME}/ - ) - foreach(cpp_include_dir ${ARG_CPP_INCLUDES}) - install( - DIRECTORY ${cpp_include_dir}/ - DESTINATION include/${PROJECT_NAME}/ - ) - endforeach() - - install( - TARGETS ${target}_conversions - EXPORT ${PROJECT_NAME} - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin - ) - ament_export_dependencies(builtin_interfaces proto2ros rclcpp) - ament_export_targets(${PROJECT_NAME}) -endmacro() diff --git a/proto2ros/cmake/rosidl_helpers.cmake b/proto2ros/cmake/rosidl_helpers.cmake deleted file mode 100644 index b7ce862..0000000 --- a/proto2ros/cmake/rosidl_helpers.cmake +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# Adds Python sources to an existing rosidl generated Python package. -# -# :param target: the target name for the file transfer, so it can be depended on. -# :param MODULES: Python modules (i.e. files) to be added. These will copied -# under the rosidl generated Python package. No directory structure will -# be kept. -# :param PACKAGES: Python packages (i.e. directories) to be added. Their content -# will be copied under the rosidl generated Python Package. Their directory -# structure will be kept. -# :param DESTINATION: the rosidl_generate_interfaces target that the destination -# Python package is associated with. Defaults to the current project name. -function(rosidl_generated_python_package_add target) - cmake_parse_arguments(ARG "" "DESTINATION" "MODULES;PACKAGES" ${ARGN}) - if(NOT ARG_DESTINATION) - set(ARG_DESTINATION ${PROJECT_NAME}) - endif() - if(NOT ARG_MODULES AND NOT ARG_PACKAGES) - message(FATAL_ERROR "No modules nor packages to add") - endif() - set(OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py/${ARG_DESTINATION}") - - if(ARG_MODULES) - unset(input_files) - unset(output_files) - foreach(module ${ARG_MODULES}) - get_filename_component(module_name "${module}" NAME) - list(APPEND output_files "${OUTPUT_DIR}/${module_name}") - get_filename_component(module_path "${module}" ABSOLUTE) - list(APPEND input_files "${module_path}") - endforeach() - add_custom_command( - OUTPUT ${output_files} - COMMAND ${CMAKE_COMMAND} -E copy ${input_files} ${OUTPUT_DIR}/. - DEPENDS ${input_files} - ) - list(APPEND OUTPUT_FILES ${output_files}) - endif() - - if(ARG_PACKAGES) - unset(input_files) - unset(output_files) - foreach(package ${ARG_PACKAGES}) - get_filename_component(package_path "${package}" ABSOLUTE) - file(GLOB_RECURSE package_files "${package_path}" *.py) - foreach(input_file ${package_files}) - get_filename_component(module_name "${module}" NAME) - file(RELATIVE_PATH output_file ${package_path} "${input_file}") - list(APPEND output_files "${OUTPUT_DIR}/${output_file}") - endforeach() - list(APPEND input_dirs "${package_path}") - list(APPEND input_files "${package_files}") - endforeach() - add_custom_command( - OUTPUT ${output_files} - COMMAND ${CMAKE_COMMAND} -E copy_directory ${input_dirs} ${OUTPUT_DIR}/. - DEPENDS ${input_files} ${input_dirs} - ) - list(APPEND OUTPUT_FILES ${output_files}) - endif() - - add_custom_target(${target} ALL DEPENDS ${OUTPUT_FILES}) - add_dependencies(${target} ${ARG_DESTINATION}) -endfunction() - -# Exports relevant variables to setup tests that depend on rosidl generated artifacts. -# -# :param LIBRARY_DIRS: the name of the variable to yield the paths -# where generated shared library may be found. -# :param ENV: the name of the variable to yield relevant environment -# variable values pointing to generated artifacts (such as PYTHONPATH). -function(get_rosidl_generated_interfaces_test_setup) - cmake_parse_arguments(ARG "" "LIBRARY_DIRS;ENV" "" ${ARGN}) - if(ARG_LIBRARY_DIRS) - set(${ARG_LIBRARY_DIRS} "${CMAKE_CURRENT_BINARY_DIR}" PARENT_SCOPE) - endif() - if(ARG_ENV) - set(${ARG_ENV} "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py" PARENT_SCOPE) - endif() -endfunction() diff --git a/proto2ros/cmake/templates/mypy.ini.in b/proto2ros/cmake/templates/mypy.ini.in deleted file mode 100644 index dfd375d..0000000 --- a/proto2ros/cmake/templates/mypy.ini.in +++ /dev/null @@ -1,10 +0,0 @@ -# Generated from cmake/templates/mypy.ini.in in the proto2ros package. -[mypy] -ignore_missing_imports = true -mypy_path = "@MYPY_PATH@" -# Ignore missing type annotations, mainly coming from ROS 2 imports. -disable_error_code = var-annotated - -[mypy-google.*] -# Ignore google.* modules explicitly to avoid missing type hints errors. -ignore_missing_imports = true diff --git a/proto2ros/include/proto2ros/conversions.hpp b/proto2ros/include/proto2ros/conversions.hpp deleted file mode 100644 index 35ff3a3..0000000 --- a/proto2ros/include/proto2ros/conversions.hpp +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) 2024 Boston Dynamics AI Institute LLC. All rights reserved. - -/// This module provides basic conversion APIs, applicable to any proto2ros generated packages. - -#pragma once - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace proto2ros::conversions { - -/// Unpacks a proto2ros/AnyProto ROS message into any Protobuf message. -/// -/// \throws std::runtime_error if the given ROS message cannot be unpacked onto the given Protobuf message. -template -void Convert(const proto2ros::msg::AnyProto& ros_msg, T* proto_msg) { - proto_msg->Clear(); - auto wrapper = google::protobuf::Any(); - wrapper.set_type_url(ros_msg.type_url); - auto* value = wrapper.mutable_value(); - value->reserve(ros_msg.value.size()); - value->assign(ros_msg.value.begin(), ros_msg.value.end()); - if (!wrapper.UnpackTo(proto_msg)) { - throw std::runtime_error("failed to unpack AnyProto message"); - } -} - -/// Packs any Protobuf message into a proto2ros/AnyProto ROS message. -template -void Convert(const T& proto_msg, proto2ros::msg::AnyProto* ros_msg) { - auto wrapper = google::protobuf::Any(); - wrapper.PackFrom(proto_msg); - ros_msg->type_url = wrapper.type_url(); - const auto& value = wrapper.value(); - ros_msg->value.reserve(value.size()); - ros_msg->value.assign(value.begin(), value.end()); -} - -/// Converts from proto2ros/AnyProto ROS message to google.protobuf.Any Protobuf messages. -void Convert(const proto2ros::msg::AnyProto& ros_msg, google::protobuf::Any* proto_msg); - -/// Converts from google.protobuf.Any Protobuf messages to proto2ros/AnyProto ROS messages. -void Convert(const google::protobuf::Any& proto_msg, proto2ros::msg::AnyProto* ros_msg); - -/// Converts from google.protobuf.Any Protobuf messages to proto2ros/AnyProto ROS messages. -void Convert(const builtin_interfaces::msg::Duration& ros_msg, google::protobuf::Duration* proto_msg); - -/// Converts from google.protobuf.Duration Protobuf messages to builtin_interfaces/Duration ROS messages. -void Convert(const google::protobuf::Duration& proto_msg, builtin_interfaces::msg::Duration* ros_msg); - -/// Converts from builtin_interfaces/Time ROS messages to google.protobuf.Timestamp Protobuf messages. -void Convert(const builtin_interfaces::msg::Time& ros_msg, google::protobuf::Timestamp* proto_msg); - -/// Converts from google.protobuf.Timestamp Protobuf messages to builtin_interfaces/Time ROS messages. -void Convert(const google::protobuf::Timestamp& proto_msg, builtin_interfaces::msg::Time* ros_msg); - -/// Converts from std_msgs/Float64 ROS messages to google.protobuf.DoubleValue Protobuf messages. -void Convert(const std_msgs::msg::Float64& ros_msg, google::protobuf::DoubleValue* proto_msg); - -/// Converts from google.protobuf.DoubleValue Protobuf messages to std_msgs/Float64 ROS messages. -void Convert(const google::protobuf::DoubleValue& proto_msg, std_msgs::msg::Float64* ros_msg); - -/// Converts from std_msgs/Float32 ROS messages to google.protobuf.FloatValue Protobuf messages. -void Convert(const std_msgs::msg::Float32& ros_msg, google::protobuf::FloatValue* proto_msg); - -/// Converts from google.protobuf.FloatValue Protobuf messages to std_msgs/Float32 ROS messages. -void Convert(const google::protobuf::FloatValue& proto_msg, std_msgs::msg::Float32* ros_msg); - -/// Converts from std_msgs/Int64 ROS messages to google.protobuf.Int64Value Protobuf messages. -void Convert(const std_msgs::msg::Int64& ros_msg, google::protobuf::Int64Value* proto_msg); - -/// Converts from google.protobuf.Int64Value Protobuf messages to std_msgs/Int64 ROS messages. -void Convert(const google::protobuf::Int64Value& proto_msg, std_msgs::msg::Int64* ros_msg); - -/// Converts from std_msgs/Int32 ROS messages to google.protobuf.Int32Value Protobuf messages. -void Convert(const std_msgs::msg::Int32& ros_msg, google::protobuf::Int32Value* proto_msg); - -/// Converts from google.protobuf.Int32Value Protobuf messages to std_msgs/Int32 ROS messages. -void Convert(const google::protobuf::Int32Value& proto_msg, std_msgs::msg::Int32* ros_msg); - -/// Converts from std_msgs/UInt64 ROS messages to google.protobuf.UInt64Value Protobuf messages. -void Convert(const std_msgs::msg::UInt64& ros_msg, google::protobuf::UInt64Value* proto_msg); - -/// Converts from google.protobuf.UInt64Value Protobuf messages to std_msgs/UInt64 ROS messages. -void Convert(const google::protobuf::UInt64Value& proto_msg, std_msgs::msg::UInt64* ros_msg); - -/// Converts from std_msgs/UInt32 ROS messages to google.protobuf.UInt32Value Protobuf messages. -void Convert(const std_msgs::msg::UInt32& ros_msg, google::protobuf::UInt32Value* proto_msg); - -/// Converts from google.protobuf.UInt32Value Protobuf messages to std_msgs/UInt32 ROS messages. -void Convert(const google::protobuf::UInt32Value& proto_msg, std_msgs::msg::UInt32* ros_msg); - -/// Converts from std_msgs/Bool ROS messages to google.protobuf.BoolValue Protobuf messages. -void Convert(const std_msgs::msg::Bool& ros_msg, google::protobuf::BoolValue* proto_msg); - -/// Converts from google.protobuf.BoolValue Protobuf messages to std_msgs/Bool ROS messages. -void Convert(const google::protobuf::BoolValue& proto_msg, std_msgs::msg::Bool* ros_msg); - -/// Converts from std_msgs/String ROS messages to google.protobuf.StringValue Protobuf messages. -void Convert(const std_msgs::msg::String& ros_msg, google::protobuf::StringValue* proto_msg); - -/// Converts from google.protobuf.StringValue Protobuf messages to std_msgs/String ROS messages. -void Convert(const google::protobuf::StringValue& proto_msg, std_msgs::msg::String* ros_msg); - -/// Converts from proto2ros/Bytes ROS messages to google.protobuf.BytesValue Protobuf messages. -void Convert(const proto2ros::msg::Bytes& ros_msg, google::protobuf::BytesValue* proto_msg); - -/// Converts from google.protobuf.BytesValue Protobuf messages to proto2ros/Bytes ROS messages. -void Convert(const google::protobuf::BytesValue& proto_msg, proto2ros::msg::Bytes* ros_msg); - -/// Converts from proto2ros/Value ROS messages to google.protobuf.Value Protobuf messages. -void Convert(const proto2ros::msg::Value& ros_msg, google::protobuf::Value* proto_msg); - -/// Converts from google.protobuf.Value Protobuf messages to proto2ros/Value ROS messages. -void Convert(const google::protobuf::Value& proto_msg, proto2ros::msg::Value* ros_msg); - -/// Converts from proto2ros/List ROS messages to google.protobuf.ListValue Protobuf messages. -void Convert(const proto2ros::msg::List& ros_msg, google::protobuf::ListValue* proto_msg); - -/// Converts from google.protobuf.ListValue Protobuf messages to proto2ros/List ROS messages. -void Convert(const google::protobuf::ListValue& proto_msg, proto2ros::msg::List* ros_msg); - -/// Converts from proto2ros/Struct ROS messages to google.protobuf.Struct Protobuf messages. -void Convert(const proto2ros::msg::Struct& ros_msg, google::protobuf::Struct* proto_msg); - -/// Converts from google.protobuf.Struct Protobuf messages to proto2ros/Struct ROS messages. -void Convert(const google::protobuf::Struct& proto_msg, proto2ros::msg::Struct* ros_msg); - -} // namespace proto2ros::conversions diff --git a/proto2ros/msg/Any.msg b/proto2ros/msg/Any.msg deleted file mode 100644 index e568cd2..0000000 --- a/proto2ros/msg/Any.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# A dynamically typed ROS 2 message. - -string type_name # ROS 2 message type name -uint8[] value # Serialized ROS 2 message instance diff --git a/proto2ros/msg/AnyProto.msg b/proto2ros/msg/AnyProto.msg deleted file mode 100644 index 881080c..0000000 --- a/proto2ros/msg/AnyProto.msg +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# A dynamically typed Protobuf message. Equivalent to the google.protobuf.Any message. -# See https://protobuf.dev/reference/protobuf/google.protobuf/#any for further reference. - -string type_url # Protobuf message type URL. -uint8[] value # Packed Protobuf message instance. diff --git a/proto2ros/msg/Bytes.msg b/proto2ros/msg/Bytes.msg deleted file mode 100644 index a835697..0000000 --- a/proto2ros/msg/Bytes.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# A binary blob. Equivalent to the google.protobuf.BytesValue message, also used to map repeated bytes fields to -# the ROS 2 domain. See https://protobuf.dev/reference/protobuf/google.protobuf/#bytes-value for further reference. - -uint8[] data diff --git a/proto2ros/msg/List.msg b/proto2ros/msg/List.msg deleted file mode 100644 index d012d59..0000000 --- a/proto2ros/msg/List.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# A list of dynamically typed values. Equivalent to the google.protobuf.ListValue message. -# See https://protobuf.dev/reference/protobuf/google.protobuf/#list-value for further reference. - -proto2ros/Value[] values diff --git a/proto2ros/msg/Struct.msg b/proto2ros/msg/Struct.msg deleted file mode 100644 index 23d382a..0000000 --- a/proto2ros/msg/Struct.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# A structured data value with dynamically typed fields. Equivalent to the google.protobuf.Struct message. -# See https://protobuf.dev/reference/protobuf/google.protobuf/#struct for further reference. - -proto2ros/StructEntry[] fields diff --git a/proto2ros/msg/StructEntry.msg b/proto2ros/msg/StructEntry.msg deleted file mode 100644 index 8d16464..0000000 --- a/proto2ros/msg/StructEntry.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# Key-value entries in the proto2ros/Struct ROS 2 message. Equivalent to -# the auxiliary Protobuf message used over-the-wire representation of the -# google.protobuf.Struct message. - -string key -proto2ros/Value value diff --git a/proto2ros/msg/Value.msg b/proto2ros/msg/Value.msg deleted file mode 100644 index 92462c4..0000000 --- a/proto2ros/msg/Value.msg +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# A dynamically typed (scalar or composite) value. Equivalent to the google.protobuf.Value message. -# See https://protobuf.dev/reference/protobuf/google.protobuf/#value for further reference. - -int8 NO_VALUE_SET=0 -int8 NULL_VALUE_SET=1 -int8 NUMBER_VALUE_SET=2 -int8 STRING_VALUE_SET=3 -int8 BOOL_VALUE_SET=4 -int8 STRUCT_VALUE_SET=5 -int8 LIST_VALUE_SET=6 - -int8 kind 0 - -float64 number_value -string string_value -bool bool_value -proto2ros/Any struct_value # is proto2ros/Struct -proto2ros/Any list_value # is proto2ros/List diff --git a/proto2ros/package.xml b/proto2ros/package.xml deleted file mode 100644 index cd238fa..0000000 --- a/proto2ros/package.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - proto2ros - 0.1.0 - Protobuf to ROS 2 interoperability interfaces - BD AI Institute - MIT - - ament_cmake - ament_cmake_python - rosidl_default_generators - rosidl_default_generators - ament_cmake_mypy - - clang-tidy - - protobuf - protobuf-dev - rclcpp - - builtin_interfaces - std_msgs - - rclpy - rosidl_adapter - - rosidl_default_runtime - - python3-inflection - python3-jinja2 - python3-multipledispatch - python3-numpy - python3-networkx - python3-protobuf - python3-yaml - - ament_cmake_pytest - - rosidl_interface_packages - - - ament_cmake - - diff --git a/proto2ros/proto2ros-extras.cmake b/proto2ros/proto2ros-extras.cmake deleted file mode 100644 index 5542598..0000000 --- a/proto2ros/proto2ros-extras.cmake +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. -include("${proto2ros_DIR}/rosidl_helpers.cmake") -if(POLICY CMP0148) - cmake_policy(SET CMP0148 OLD) # to accommodate rosidl pipeline -endif() - -find_package(Python3 REQUIRED) -find_package(PythonInterp REQUIRED) -find_package(Protobuf REQUIRED) -if(BUILD_TESTING) - find_package(ament_cmake_mypy QUIET) -endif() -include("${proto2ros_DIR}/proto2ros_generate.cmake") - -find_package(rclcpp REQUIRED) -find_package(builtin_interfaces REQUIRED) -find_package(rosidl_default_generators REQUIRED) -include("${proto2ros_DIR}/proto2ros_vendor_package.cmake") diff --git a/proto2ros/proto2ros/cli/__init__.py b/proto2ros/proto2ros/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/proto2ros/proto2ros/cli/generate.py b/proto2ros/proto2ros/cli/generate.py deleted file mode 100644 index 747de74..0000000 --- a/proto2ros/proto2ros/cli/generate.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. -import argparse -import collections -import importlib -import importlib.resources -import os -import pathlib -import sys -import textwrap -from typing import List - -from google.protobuf.descriptor_pb2 import FileDescriptorProto - -from proto2ros.configuration import Configuration -from proto2ros.dependencies import fix_dependency_cycles -from proto2ros.descriptors.sources import read_source_descriptors -from proto2ros.equivalences import Equivalence, all_message_specifications, extract_equivalences -from proto2ros.output.cpp import dump_conversions_cpp_sources, to_pb2_cpp_header, to_ros_cpp_header -from proto2ros.output.interfaces import dump_message_specification, which_message_specification -from proto2ros.output.python import ( - dump_conversions_python_module, - dump_specifications_python_module, - to_pb2_python_module_name, - to_ros_python_module_name, -) - - -def do_generate(args: argparse.Namespace) -> int: - """Primary function to execute conversion of protobufs to ros msgs.""" - # Fetch baseline configuration. - config = Configuration.from_file(args.config_file) - - # Read source descriptors for processing. - source_descriptors: List[FileDescriptorProto] = [] - for descriptor_file in args.descriptor_files: - for source_descriptor in read_source_descriptors(descriptor_file): - # Map all Protobuf packages in source descriptors to target ROS package. - config.package_mapping[source_descriptor.package] = args.package_name - if not config.skip_implicit_imports: - # Collect all .proto source files (including those imported). - source_paths = [source_descriptor.name, *source_descriptor.dependency] - # Add corresponding *_pb2 Python modules to configured Python imports. - config.python_imports.update(map(to_pb2_python_module_name, source_paths)) - # Add corresponding *.pb.h C++ headers to configured C++ headers. - config.cpp_headers.update(map(to_pb2_cpp_header, source_paths)) - source_descriptors.append(source_descriptor) - - # Apply overlays to configuration. - for overlay_file in args.config_overlay_files: - config.update(**Configuration.updates_from_file(overlay_file)) - - # Compute Protobuf <-> ROS equivalences. - equivalences: List[Equivalence] = [] - for source_descriptor in source_descriptors: - equivalences.extend(extract_equivalences(source_descriptor, config)) - - # Extract annotated message specifications from equivalences. - message_specifications = list(all_message_specifications(equivalences)) - fix_dependency_cycles(message_specifications, quiet=args.dry) - - if not config.skip_implicit_imports: - # Add target ROS package to configured Python imports. - config.python_imports.add(to_ros_python_module_name(args.package_name)) - # Add target ROS package to configured C++ headers. - for spec in message_specifications: - config.cpp_headers.add(to_ros_cpp_header(spec)) - - # Collect all known message specifications. - known_message_specifications = list(message_specifications) - for module_name in config.package_specifications: - module = importlib.import_module(module_name) - known_message_specifications.extend(module.messages) - - # Ensure no name clashes between message specifications. - message_types = [spec.base_type for spec in known_message_specifications] - message_type_instances = collections.Counter(message_types) - if len(message_type_instances) != len(message_types): - unique_message_type_instances = collections.Counter(set(message_type_instances)) - message_type_duplicates = list(message_type_instances - unique_message_type_instances) - print("Found duplicate message types (name clashes?):", file=sys.stderr) - print( - textwrap.indent( - "\n".join( - sorted( - [ - f"{spec.annotations['proto-type']} maps to {spec.base_type}" - for spec in known_message_specifications - if spec.base_type in message_type_duplicates - ], - ), - ), - " ", - ), - file=sys.stderr, - ) - return 1 - - files_written: List[os.PathLike] = [] - - # Write message specifications to .py file. - specifications_python_file = args.output_directory / "specifications.py" - if not args.dry: - specifications_python_file.write_text(dump_specifications_python_module(message_specifications, config) + "\n") - files_written.append(specifications_python_file) - - messages_output_directory = args.output_directory / "msg" - if args.force_message_gen or not args.dry: - messages_output_directory.mkdir(exist_ok=True) - - # Write message specifications to .msg files. - for message_specification in message_specifications: - message_output_file = which_message_specification(message_specification, messages_output_directory) - if args.force_message_gen or not args.dry: - message_output_file.write_text(dump_message_specification(message_specification) + "\n") - files_written.append(message_output_file) - - # Write Python conversion APIs .py file. - conversions_python_file = args.output_directory / "conversions.py" - if not args.dry: - conversions_python_file.write_text( - dump_conversions_python_module(message_specifications, known_message_specifications, config) + "\n", - ) - files_written.append(conversions_python_file) - - # Write C++ conversion APIs source files. - conversions_hpp_file = args.output_directory / "conversions.hpp" - conversions_cpp_file = args.output_directory / "conversions.cpp" - if not args.dry: - hpp_content, cpp_content = dump_conversions_cpp_sources( - args.package_name, - message_specifications, - known_message_specifications, - config, - ) - conversions_hpp_file.write_text(hpp_content + "\n") - conversions_cpp_file.write_text(cpp_content + "\n") - files_written.append(conversions_hpp_file) - files_written.append(conversions_cpp_file) - - if args.manifest_file: - # Write generation manifest file. - args.manifest_file.write_text("\n".join(map(str, files_written)) + "\n") - return 0 - - -def main() -> int: - """Entrypoint for proto2ros""" - parser = argparse.ArgumentParser(description="Generate Protobuf <-> ROS 2 interoperability interfaces") - parser.add_argument( - "-O", - "--output-directory", - type=pathlib.Path, - default=".", - help="Output directory for all generated files.", - ) - parser.add_argument( - "-c", - "--config-file", - type=pathlib.Path, - default=importlib.resources.path("proto2ros.configuration", "default.yaml"), - help="Base configuration file for the generation procedure.", - ) - parser.add_argument( - "-a", - "--config-overlay-file", - dest="config_overlay_files", - type=pathlib.Path, - action="append", - default=[], - help="Optional configuration overlay files.", - ) - parser.add_argument( - "-m", - "--manifest-file", - type=pathlib.Path, - default=None, - help="Optional manifest file to track generated files.", - ) - parser.add_argument( - "-d", - "--dry", - action="store_true", - default=False, - help="Whether to perform a dry run or not (manifest is still written)", - ) - parser.add_argument( - "--force-message-gen", - action="store_true", - default=False, - help="Whether to generate message files regardless of other flags.", - ) - parser.add_argument("package_name", help="Name of the ROS package that will bear generated messages.") - parser.add_argument( - "descriptor_files", - type=pathlib.Path, - nargs="+", - help="Protobuf descriptor files to process, as generated by protoc.", - ) - args = parser.parse_args() - return do_generate(args) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/proto2ros/proto2ros/compatibility.py b/proto2ros/proto2ros/compatibility.py deleted file mode 100644 index f14bca8..0000000 --- a/proto2ros/proto2ros/compatibility.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -# ruff: noqa -# fmt: off - -# NOTE(mhidalgo): handle https://bugs.launchpad.net/ubuntu/+source/networkx/+bug/2002660 -import numpy -numpy.int = numpy.int_ - -import networkx diff --git a/proto2ros/proto2ros/configuration/__init__.py b/proto2ros/proto2ros/configuration/__init__.py deleted file mode 100644 index 39cabab..0000000 --- a/proto2ros/proto2ros/configuration/__init__.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module defines configuration data structures for proto2ros generation.""" - -import dataclasses -import os -from typing import Any, Dict, Set, Union - -import yaml -from rosidl_adapter.parser import MessageSpecification - - -@dataclasses.dataclass -class Configuration: - """Mutable Protobuf <-> ROS configuration. - - Attributes: - - drop_deprecated: whether to drop deprecated fields on conversion or not. - If not dropped, deprecated fields are annotated with a comment. - passthrough_unknown: whether to forward Protobuf messages for which no - equivalent ROS message is known as a type erased, ``proto2ros/AnyProto`` - field or not. - message_mapping: a mapping from fully qualified Protobuf message type names - to fully qualified ROS message type names. This mapping comes first during - composite type translation. - package_mapping: a mapping from Protobuf package names to ROS package names, - to tell where a ROS equivalent for a Protobuf construct will be found. Note - that no checks for package existence are performed. This mapping comes - second during composite type translation (i.e. when direct message mapping - fails). - any_expansions: a mapping from fully qualified Protobuf field names (i.e. a - fully qualified Protobuf message type name followed by a dot "." followed by - the field name) of ``google.protobuf.Any`` type to Protobuf message type sets - that these fields are expected to pack. A single Protobuf message type may also - be specified in lieu of a single element set. All Protobuf message types must be - fully qualified. - allow_any_casts: when a single Protobuf message type is specified in an any expansion, - allowing any casts means to allow using the equivalent ROS message type instead of a - a type erased, ``proto2ros/Any`` field. - package_specifications: set of Python modules to gather message specifications from. - Necessary to cascade message generation for interdependent packages. - python_imports: set of Python modules to be imported (as ``import ``) in - generated conversion modules. Typically, Protobuf and ROS message Python modules. - inline_python_imports: set of Python modules to be imported into moodule scope - (as ``from import *``) in generated conversion modules. Typically, - conversion Python modules. - skip_implicit_imports: whether to skip importing Python modules for Protobuf and ROS - packages known in generated conversion modules or not. - """ - - drop_deprecated: bool = False - passthrough_unknown: bool = True - package_mapping: Dict[str, str] = dataclasses.field(default_factory=dict) - message_mapping: Dict[str, str] = dataclasses.field(default_factory=dict) - - any_expansions: Dict[str, Union[Set[str], str]] = dataclasses.field(default_factory=dict) - allow_any_casts: bool = True - - package_specifications: Set[str] = dataclasses.field(default_factory=set) - - cpp_headers: Set[str] = dataclasses.field(default_factory=set) - inline_cpp_namespaces: Set[str] = dataclasses.field(default_factory=set) - - python_imports: Set[str] = dataclasses.field(default_factory=set) - inline_python_imports: Set[str] = dataclasses.field(default_factory=set) - - skip_implicit_imports: bool = False - - def __post_init__(self) -> None: - """Enforces attribute types.""" - self.any_expansions = { - key: set(value) if not isinstance(value, str) else value for key, value in self.any_expansions.items() - } - self.cpp_headers = set(self.cpp_headers) - self.inline_cpp_namespaces = set(self.inline_cpp_namespaces) - self.python_imports = set(self.python_imports) - self.inline_python_imports = set(self.inline_python_imports) - self.package_specifications = set(self.package_specifications) - - def update(self, **attributes: Any) -> None: - """Updates configuration attributes with a shallow merge.""" - for name, value in attributes.items(): - old_value = getattr(self, name) - if hasattr(old_value, "update"): - old_value.update(value) - elif hasattr(old_value, "extend"): - old_value.extend(value) - else: - setattr(self, name, value) - - @classmethod - def updates_from_file(cls, path: os.PathLike) -> Dict[str, Any]: - """Reads configuration attribute updates from a file.""" - with open(path, "r") as f: - return yaml.safe_load(f) - - @classmethod - def from_file(cls, path: os.PathLike) -> "Configuration": - """Reads configuration from a file.""" - return cls(**cls.updates_from_file(path)) diff --git a/proto2ros/proto2ros/configuration/default.yaml b/proto2ros/proto2ros/configuration/default.yaml deleted file mode 100644 index 1fc770b..0000000 --- a/proto2ros/proto2ros/configuration/default.yaml +++ /dev/null @@ -1,27 +0,0 @@ -message_mapping: - google.protobuf.Any: proto2ros/AnyProto - google.protobuf.Timestamp: builtin_interfaces/Time - google.protobuf.Duration: builtin_interfaces/Duration - google.protobuf.DoubleValue: std_msgs/Float64 - google.protobuf.FloatValue: std_msgs/Float32 - google.protobuf.Int64Value: std_msgs/Int64 - google.protobuf.UInt64Value: std_msgs/UInt64 - google.protobuf.Int32Value: std_msgs/Int32 - google.protobuf.UInt32Value: std_msgs/UInt32 - google.protobuf.BoolValue: std_msgs/Bool - google.protobuf.StringValue: std_msgs/String - google.protobuf.BytesValue: proto2ros/Bytes - google.protobuf.ListValue: proto2ros/List - google.protobuf.Value: proto2ros/Value - google.protobuf.Struct: proto2ros/Struct -python_imports: - - std_msgs.msg - - proto2ros.msg - - builtin_interfaces.msg - - google.protobuf.any_pb2 - - google.protobuf.duration_pb2 - - google.protobuf.struct_pb2 - - google.protobuf.timestamp_pb2 - - google.protobuf.wrappers_pb2 -inline_python_imports: - - proto2ros.conversions.basic diff --git a/proto2ros/proto2ros/conversions/__init__.py b/proto2ros/proto2ros/conversions/__init__.py deleted file mode 100644 index a579f2f..0000000 --- a/proto2ros/proto2ros/conversions/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. -import multipledispatch - -# Overloads entrypoint (via multiple dispatch) -convert = multipledispatch.Dispatcher("convert") diff --git a/proto2ros/proto2ros/conversions/basic.py b/proto2ros/proto2ros/conversions/basic.py deleted file mode 100644 index d5f05be..0000000 --- a/proto2ros/proto2ros/conversions/basic.py +++ /dev/null @@ -1,486 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides basic conversion APIs, applicable to any proto2ros generated packages.""" - -from typing import Any - -import builtin_interfaces.msg -import google.protobuf.any_pb2 -import google.protobuf.duration_pb2 -import google.protobuf.struct_pb2 -import google.protobuf.timestamp_pb2 -import google.protobuf.wrappers_pb2 -import rclpy -import std_msgs.msg - -import proto2ros.msg -from proto2ros.conversions import convert - - -@convert.register(proto2ros.msg.AnyProto, object) -def convert_proto2ros_any_proto_message_to_some_proto(ros_msg: proto2ros.msg.AnyProto, proto_msg: Any) -> None: - """Unpacks a proto2ros/AnyProto ROS message into any Protobuf message. - - Raises: - ValueError: if the given ROS message cannot be unpacked onto the given Protobuf message. - """ - proto_msg.Clear() - wrapper = google.protobuf.any_pb2.Any() - wrapper.type_url = ros_msg.type_url - wrapper.value = ros_msg.value.tobytes() - if not wrapper.Unpack(proto_msg): - raise ValueError(f"failed to convert {ros_msg} to {proto_msg}") - - -@convert.register(object, proto2ros.msg.AnyProto) -def convert_some_proto_to_proto2ros_any_proto_message(proto_msg: Any, ros_msg: proto2ros.msg.AnyProto) -> None: - """Packs any Protobuf message into a proto2ros/AnyProto ROS message.""" - wrapper = google.protobuf.any_pb2.Any() - wrapper.Pack(proto_msg) - ros_msg.type_url = wrapper.type_url - ros_msg.value = wrapper.value - - -@convert.register(proto2ros.msg.AnyProto, proto2ros.msg.AnyProto) -def _(proto_msg: proto2ros.msg.AnyProto, ros_msg: proto2ros.msg.AnyProto) -> None: - # address multipledispatch ambiguous resolution concerns - raise RuntimeError("invalid overload") - - -@convert.register(proto2ros.msg.AnyProto, google.protobuf.any_pb2.Any) -def convert_proto2ros_any_proto_message_to_google_protobuf_any_proto( - ros_msg: proto2ros.msg.AnyProto, - proto_msg: google.protobuf.any_pb2.Any, -) -> None: - """Converts from proto2ros/AnyProto ROS message to google.protobuf.Any Protobuf messages.""" - proto_msg.Clear() - proto_msg.type_url = ros_msg.type_url - proto_msg.value = ros_msg.value.tobytes() - - -@convert.register(google.protobuf.any_pb2.Any, proto2ros.msg.AnyProto) -def convert_google_protobuf_any_proto_to_proto2ros_any_proto_message( - proto_msg: google.protobuf.any_pb2.Any, - ros_msg: proto2ros.msg.AnyProto, -) -> None: - """Converts from google.protobuf.Any Protobuf messages to proto2ros/AnyProto ROS messages.""" - ros_msg.type_url = proto_msg.type_url - ros_msg.value = proto_msg.value - - -@convert.register(builtin_interfaces.msg.Duration, google.protobuf.duration_pb2.Duration) -def convert_builtin_interfaces_duration_message_to_google_protobuf_duration_proto( - ros_msg: builtin_interfaces.msg.Duration, - proto_msg: google.protobuf.duration_pb2.Duration, -) -> None: - """Converts from google.protobuf.Any Protobuf messages to proto2ros/AnyProto ROS messages.""" - proto_msg.seconds = ros_msg.sec - proto_msg.nanos = ros_msg.nanosec - - -convert_builtin_interfaces_duration_to_proto = ( - convert_builtin_interfaces_duration_message_to_google_protobuf_duration_proto -) - - -@convert.register(google.protobuf.duration_pb2.Duration, builtin_interfaces.msg.Duration) -def convert_google_protobuf_duration_proto_to_builtin_interfaces_duration_message( - proto_msg: google.protobuf.duration_pb2.Duration, - ros_msg: builtin_interfaces.msg.Duration, -) -> None: - """Converts from google.protobuf.Duration Protobuf messages to builtin_interfaces/Duration ROS messages.""" - ros_msg.sec = proto_msg.seconds - ros_msg.nanosec = proto_msg.nanos - - -convert_proto_to_builtin_interfaces_duration = ( - convert_google_protobuf_duration_proto_to_builtin_interfaces_duration_message -) - - -@convert.register(builtin_interfaces.msg.Time, google.protobuf.timestamp_pb2.Timestamp) -def convert_builtin_interfaces_time_message_to_google_protobuf_timestamp_proto( - ros_msg: builtin_interfaces.msg.Time, - proto_msg: google.protobuf.timestamp_pb2.Timestamp, -) -> None: - """Converts from builtin_interfaces/Time ROS messages to google.protobuf.Timestamp Protobuf messages.""" - proto_msg.seconds = ros_msg.sec - proto_msg.nanos = ros_msg.nanosec - - -convert_builtin_interfaces_time_to_proto = convert_builtin_interfaces_time_message_to_google_protobuf_timestamp_proto - - -@convert.register(google.protobuf.timestamp_pb2.Timestamp, builtin_interfaces.msg.Time) -def convert_google_protobuf_timestamp_proto_to_builtin_interfaces_time_message( - proto_msg: google.protobuf.timestamp_pb2.Timestamp, - ros_msg: builtin_interfaces.msg.Time, -) -> None: - """Converts from google.protobuf.Timestamp Protobuf messages to builtin_interfaces/Time ROS messages.""" - ros_msg.sec = proto_msg.seconds - ros_msg.nanosec = proto_msg.nanos - - -convert_proto_to_builtin_interfaces_time = convert_google_protobuf_timestamp_proto_to_builtin_interfaces_time_message - - -@convert.register(std_msgs.msg.Float64, google.protobuf.wrappers_pb2.DoubleValue) -def convert_std_msgs_float64_message_to_google_protobuf_double_value_proto( - ros_msg: std_msgs.msg.Float64, - proto_msg: google.protobuf.wrappers_pb2.DoubleValue, -) -> None: - """Converts from std_msgs/Float64 ROS messages to google.protobuf.DoubleValue Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_float64_to_proto = convert_std_msgs_float64_message_to_google_protobuf_double_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.DoubleValue, std_msgs.msg.Float64) -def convert_google_protobuf_double_value_proto_to_std_msgs_float64_message( - proto_msg: google.protobuf.wrappers_pb2.DoubleValue, - ros_msg: std_msgs.msg.Float64, -) -> None: - """Converts from google.protobuf.DoubleValue Protobuf messages to std_msgs/Float64 ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_float64 = convert_google_protobuf_double_value_proto_to_std_msgs_float64_message - - -@convert.register(std_msgs.msg.Float32, google.protobuf.wrappers_pb2.FloatValue) -def convert_std_msgs_float32_message_to_google_protobuf_float_value_proto( - ros_msg: std_msgs.msg.Float32, - proto_msg: google.protobuf.wrappers_pb2.FloatValue, -) -> None: - """Converts from std_msgs/Float32 ROS messages to google.protobuf.FloatValue Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_float32_to_proto = convert_std_msgs_float32_message_to_google_protobuf_float_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.FloatValue, std_msgs.msg.Float32) -def convert_google_protobuf_float_value_proto_to_std_msgs_float32_message( - proto_msg: google.protobuf.wrappers_pb2.FloatValue, - ros_msg: std_msgs.msg.Float32, -) -> None: - """Converts from google.protobuf.FloatValue Protobuf messages to std_msgs/Float32 ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_float32 = convert_google_protobuf_float_value_proto_to_std_msgs_float32_message - - -@convert.register(std_msgs.msg.Int64, google.protobuf.wrappers_pb2.Int64Value) -def convert_std_msgs_int64_message_to_google_protobuf_int64_value_proto( - ros_msg: std_msgs.msg.Int64, - proto_msg: google.protobuf.wrappers_pb2.Int64Value, -) -> None: - """Converts from std_msgs/Int64 ROS messages to google.protobuf.Int64Value Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_int64_to_proto = convert_std_msgs_int64_message_to_google_protobuf_int64_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.Int64Value, std_msgs.msg.Int64) -def convert_google_protobuf_int64_value_proto_to_std_msgs_int64_message( - proto_msg: google.protobuf.wrappers_pb2.Int64Value, - ros_msg: std_msgs.msg.Int64, -) -> None: - """Converts from google.protobuf.Int64Value Protobuf messages to std_msgs/Int64 ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_int64 = convert_google_protobuf_int64_value_proto_to_std_msgs_int64_message - - -@convert.register(std_msgs.msg.Int32, google.protobuf.wrappers_pb2.Int32Value) -def convert_std_msgs_int32_message_to_google_protobuf_int32_value_proto( - ros_msg: std_msgs.msg.Int32, - proto_msg: google.protobuf.wrappers_pb2.Int32Value, -) -> None: - """Converts from std_msgs/Int32 ROS messages to google.protobuf.Int32Value Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_int32_to_proto = convert_std_msgs_int32_message_to_google_protobuf_int32_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.Int32Value, std_msgs.msg.Int32) -def convert_google_protobuf_int32_value_proto_to_std_msgs_int32_message( - proto_msg: google.protobuf.wrappers_pb2.Int32Value, - ros_msg: std_msgs.msg.Int32, -) -> None: - """Converts from google.protobuf.Int32Value Protobuf messages to std_msgs/Int32 ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_int32 = convert_google_protobuf_int32_value_proto_to_std_msgs_int32_message - - -@convert.register(std_msgs.msg.UInt64, google.protobuf.wrappers_pb2.UInt64Value) -def convert_std_msgs_uint64_message_to_google_protobuf_uint64_value_proto( - ros_msg: std_msgs.msg.UInt64, - proto_msg: google.protobuf.wrappers_pb2.UInt64Value, -) -> None: - """Converts from std_msgs/UInt64 ROS messages to google.protobuf.UInt64Value Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_uint64_to_proto = convert_std_msgs_uint64_message_to_google_protobuf_uint64_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.UInt64Value, std_msgs.msg.UInt64) -def convert_google_protobuf_uint64_value_proto_to_std_msgs_uint64_message( - proto_msg: google.protobuf.wrappers_pb2.UInt64Value, - ros_msg: std_msgs.msg.UInt64, -) -> None: - """Converts from google.protobuf.UInt64Value Protobuf messages to std_msgs/UInt64 ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_uint64 = convert_google_protobuf_uint64_value_proto_to_std_msgs_uint64_message - - -@convert.register(std_msgs.msg.UInt32, google.protobuf.wrappers_pb2.UInt32Value) -def convert_std_msgs_uint32_message_to_google_protobuf_uint32_value_proto( - ros_msg: std_msgs.msg.UInt32, - proto_msg: google.protobuf.wrappers_pb2.UInt32Value, -) -> None: - """Converts from std_msgs/UInt32 ROS messages to google.protobuf.UInt32Value Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_uint32_to_proto = convert_std_msgs_uint32_message_to_google_protobuf_uint32_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.UInt32Value, std_msgs.msg.UInt32) -def convert_google_protobuf_uint32_value_proto_to_std_msgs_uint32_message( - proto_msg: google.protobuf.wrappers_pb2.UInt32Value, - ros_msg: std_msgs.msg.UInt32, -) -> None: - """Converts from google.protobuf.UInt32Value Protobuf messages to std_msgs/UInt32 ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_uint32 = convert_google_protobuf_uint32_value_proto_to_std_msgs_uint32_message - - -@convert.register(std_msgs.msg.Bool, google.protobuf.wrappers_pb2.BoolValue) -def convert_std_msgs_bool_message_to_google_protobuf_bool_value_proto( - ros_msg: std_msgs.msg.Bool, - proto_msg: google.protobuf.wrappers_pb2.BoolValue, -) -> None: - """Converts from std_msgs/Bool ROS messages to google.protobuf.BoolValue Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_bool_to_proto = convert_std_msgs_bool_message_to_google_protobuf_bool_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.BoolValue, std_msgs.msg.Bool) -def convert_google_protobuf_bool_value_proto_to_std_msgs_bool_message( - proto_msg: google.protobuf.wrappers_pb2.BoolValue, - ros_msg: std_msgs.msg.Bool, -) -> None: - """Converts from google.protobuf.BoolValue Protobuf messages to std_msgs/Bool ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_bool = convert_google_protobuf_bool_value_proto_to_std_msgs_bool_message - - -@convert.register(std_msgs.msg.String, google.protobuf.wrappers_pb2.StringValue) -def convert_std_msgs_string_message_to_google_protobuf_string_value_proto( - ros_msg: std_msgs.msg.String, - proto_msg: google.protobuf.wrappers_pb2.StringValue, -) -> None: - """Converts from std_msgs/String ROS messages to google.protobuf.StringValue Protobuf messages.""" - proto_msg.value = ros_msg.data - - -convert_std_msgs_string_to_proto = convert_std_msgs_string_message_to_google_protobuf_string_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.StringValue, std_msgs.msg.String) -def convert_google_protobuf_string_value_proto_to_std_msgs_string_message( - proto_msg: google.protobuf.wrappers_pb2.StringValue, - ros_msg: std_msgs.msg.String, -) -> None: - """Converts from google.protobuf.StringValue Protobuf messages to std_msgs/String ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_std_msgs_string = convert_google_protobuf_string_value_proto_to_std_msgs_string_message - - -@convert.register(proto2ros.msg.Bytes, google.protobuf.wrappers_pb2.BytesValue) -def convert_proto2ros_bytes_message_to_google_protobuf_bytes_value_proto( - ros_msg: proto2ros.msg.Bytes, - proto_msg: google.protobuf.wrappers_pb2.BytesValue, -) -> None: - """Converts from proto2ros/Bytes ROS messages to google.protobuf.BytesValue Protobuf messages.""" - proto_msg.value = ros_msg.data.tobytes() - - -convert_proto2ros_bytes_to_proto = convert_proto2ros_bytes_message_to_google_protobuf_bytes_value_proto - - -@convert.register(google.protobuf.wrappers_pb2.BytesValue, proto2ros.msg.Bytes) -def convert_google_protobuf_bytes_value_proto_to_proto2ros_bytes_message( - proto_msg: google.protobuf.wrappers_pb2.BytesValue, - ros_msg: proto2ros.msg.Bytes, -) -> None: - """Converts from google.protobuf.BytesValue Protobuf messages to proto2ros/Bytes ROS messages.""" - ros_msg.data = proto_msg.value - - -convert_proto_to_proto2ros_bytes = convert_google_protobuf_bytes_value_proto_to_proto2ros_bytes_message - - -@convert.register(proto2ros.msg.Value, google.protobuf.struct_pb2.Value) -def convert_proto2ros_value_message_to_google_protobuf_value_proto( - ros_msg: proto2ros.msg.Value, - proto_msg: google.protobuf.struct_pb2.Value, -) -> None: - """Converts from proto2ros/Value ROS messages to google.protobuf.Value Protobuf messages.""" - if ros_msg.kind == proto2ros.msg.Value.NUMBER_VALUE_SET: - proto_msg.number_value = ros_msg.number_value - elif ros_msg.kind == proto2ros.msg.Value.STRING_VALUE_SET: - proto_msg.string_value = ros_msg.string_value - elif ros_msg.kind == proto2ros.msg.Value.BOOL_VALUE_SET: - proto_msg.bool_value = ros_msg.bool_value - elif ros_msg.kind == proto2ros.msg.Value.STRUCT_VALUE_SET: - if proto_msg.struct_value.type_name != "proto2ros/Struct": - raise ValueError( - f"expected proto2ros/Struct message for struct_value member, got {proto_msg.struct_value.type}", - ) - typed_field_message = rclpy.serialization.deserialize_message( - proto_msg.struct_value.value.tobytes(), - proto2ros.msg.Struct, - ) - convert_proto2ros_struct_message_to_google_protobuf_struct_proto( - typed_field_message, - proto_msg.struct_value, - ) - elif ros_msg.kind == proto2ros.msg.Value.LIST_VALUE_SET: - if proto_msg.list_value.type_name != "proto2ros/List": - raise ValueError( - f"expected proto2ros/Struct message for list_value member, got {proto_msg.list_value.type}", - ) - typed_field_message = rclpy.serialization.deserialize_message( - proto_msg.list_value.value.tobytes(), - proto2ros.msg.List, - ) - convert_proto2ros_list_message_to_google_protobuf_list_value_proto( - typed_field_message, - proto_msg.list_value, - ) - elif ros_msg.kind == proto2ros.msg.Value.NO_VALUE_SET: - proto_msg.null_value = google.protobuf.struct_pb2.NullValue.NULL_VALUE - else: - raise ValueError(f"unexpected value in kind member: {ros_msg.kind}") - - -convert_proto2ros_value_to_proto = convert_proto2ros_value_message_to_google_protobuf_value_proto - - -@convert.register(google.protobuf.struct_pb2.Value, proto2ros.msg.Value) -def convert_google_protobuf_value_proto_to_proto2ros_value_message( - proto_msg: google.protobuf.struct_pb2.Value, - ros_msg: proto2ros.msg.Value, -) -> None: - """Converts from google.protobuf.Value Protobuf messages to proto2ros/Value ROS messages.""" - which = proto_msg.WhichOneOf("kind") - if which == "null_value": - ros_msg.kind = proto2ros.msg.Value.NO_VALUE_SET - elif which == "number_value": - ros_msg.number_value = proto_msg.number_value - ros_msg.kind = proto2ros.msg.Value.NUMBER_VALUE_SET - elif which == "string_value": - ros_msg.string_value = proto_msg.string_value - ros_msg.kind = proto2ros.msg.Value.STRING_VALUE_SET - elif which == "bool_value": - ros_msg.bool_value = proto_msg.bool_value - ros_msg.kind = proto2ros.msg.Value.BOOL_VALUE_SET - elif which == "struct_value": - typed_struct_message = proto2ros.msg.Struct() - convert_google_protobuf_struct_proto_to_proto2ros_struct_message( - proto_msg.struct_value, - typed_struct_message, - ) - ros_msg.struct_value.value = rclpy.serialization.serialize_message(typed_struct_message) - ros_msg.struct_value.type_name = "proto2ros/Struct" - ros_msg.kind = proto2ros.msg.Value.STRUCT_VALUE_SET - elif which == "list_value": - typed_list_message = proto2ros.msg.List() - convert_google_protobuf_list_value_proto_to_proto2ros_list_message(proto_msg.list_value, typed_list_message) - ros_msg.list_value.value = rclpy.serialization.serialize_message(typed_list_message) - ros_msg.list_value.type_name = "proto2ros/List" - ros_msg.kind = proto2ros.msg.Value.LIST_VALUE_SET - else: - raise ValueError("unexpected one-of field: " + proto_msg.WhichOneOf("kind")) - - -convert_proto_to_proto2ros_value = convert_google_protobuf_value_proto_to_proto2ros_value_message - - -@convert.register(proto2ros.msg.List, google.protobuf.struct_pb2.ListValue) -def convert_proto2ros_list_message_to_google_protobuf_list_value_proto( - ros_msg: proto2ros.msg.List, - proto_msg: google.protobuf.struct_pb2.ListValue, -) -> None: - """Converts from proto2ros/List ROS messages to google.protobuf.ListValue Protobuf messages.""" - proto_msg.Clear() - for input_item in ros_msg.values: - output_item = proto_msg.values.add() - convert_proto2ros_value_message_to_google_protobuf_value_proto(input_item, output_item) - - -convert_proto2ros_list_to_proto = convert_proto2ros_list_message_to_google_protobuf_list_value_proto - - -@convert.register(google.protobuf.struct_pb2.ListValue, proto2ros.msg.List) -def convert_google_protobuf_list_value_proto_to_proto2ros_list_message( - proto_msg: google.protobuf.struct_pb2.ListValue, - ros_msg: proto2ros.msg.List, -) -> None: - """Converts from google.protobuf.ListValue Protobuf messages to proto2ros/List ROS messages.""" - for input_item in proto_msg.values: - output_item = proto2ros.msg.Value() - convert_google_protobuf_value_proto_to_proto2ros_value_message(input_item, output_item) - ros_msg.values.append(output_item) - - -convert_proto_to_proto2ros_list = convert_google_protobuf_list_value_proto_to_proto2ros_list_message - - -@convert.register(proto2ros.msg.Struct, google.protobuf.struct_pb2.Struct) -def convert_proto2ros_struct_message_to_google_protobuf_struct_proto( - ros_msg: proto2ros.msg.Struct, - proto_msg: google.protobuf.struct_pb2.Struct, -) -> None: - """Converts from proto2ros/Struct ROS messages to google.protobuf.Struct Protobuf messages.""" - proto_msg.Clear() - for field in ros_msg.fields: - proto_msg.fields[field.key].CopyFrom(field.value) - - -convert_proto2ros_struct_to_proto = convert_proto2ros_struct_message_to_google_protobuf_struct_proto - - -@convert.register(google.protobuf.struct_pb2.Struct, proto2ros.msg.Struct) -def convert_google_protobuf_struct_proto_to_proto2ros_struct_message( - proto_msg: google.protobuf.struct_pb2.Struct, - ros_msg: proto2ros.msg.Struct, -) -> None: - """Converts from google.protobuf.Struct Protobuf messages to proto2ros/Struct ROS messages.""" - for key, value in proto_msg.fields.items(): - field = proto2ros.msg.StructEntry(key=key) - convert_google_protobuf_value_proto_to_proto2ros_value_message(value, field.value) - ros_msg.fields.append(field) - - -convert_proto_to_proto2ros_struct = convert_google_protobuf_struct_proto_to_proto2ros_struct_message diff --git a/proto2ros/proto2ros/dependencies.py b/proto2ros/proto2ros/dependencies.py deleted file mode 100644 index 0eaa155..0000000 --- a/proto2ros/proto2ros/dependencies.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides APIs to manipulate dependencies between Protobuf <-> ROS message equivalences.""" - -import collections -import itertools -import warnings -from typing import List - -from rosidl_adapter.parser import MessageSpecification - -from proto2ros.compatibility import networkx as nx -from proto2ros.utilities import to_ros_base_type - - -def message_dependency_graph(message_specs: List[MessageSpecification]) -> nx.DiGraph: - """Returns the dependency graph for the given ROS message specifications. - - This dependency graph is a directed multi-graph where message types make up nodes - and composition relationships (has-a) make up edges. Nodes are annotated with the - corresponding message specification, while edges are annotated with the corresponding - field specification. - """ - dependency_graph = nx.MultiDiGraph() - for message in message_specs: - dependency_graph.add_node(str(message.base_type), message=message) - for field in message.fields: - if field.type.is_primitive_type(): - continue - dependency_graph.add_edge(str(message.base_type), to_ros_base_type(field.type), field=field) - return dependency_graph - - -def fix_dependency_cycles(message_specs: List[MessageSpecification], quiet: bool = True) -> None: - """Fixes dependency cycles among ROS message specifications. - - ROS messages do not support recursive definitions, so this function works around this - limitation by type erasing the least amount of offending fields. - """ - dependency_graph = message_dependency_graph(message_specs) - - cycles = [] - for cycle in nx.simple_cycles(dependency_graph): - if not quiet: - message_types = [dependency_graph.nodes[node]["message"].base_type for node in cycle] - dependency_cycle_depiction = " -> ".join(str(type_) for type_ in message_types) - dependency_cycle_depiction += " -> " + str(message_types[0]) # close the loop - warnings.warn("Dependency cycle found: " + dependency_cycle_depiction, stacklevel=1) - cycles.append(cycle) - - counter = collections.Counter( - sorted( - sorted(itertools.chain(*cycles)), # ensures an stable order (maintained by sorted) - key=dependency_graph.in_degree, # implicitly breaks ties by prioritizing the least common messages - ), - ) - while counter.total() > 0: - for node, _ in counter.most_common(): # greedily break cycles - message = dependency_graph.nodes[node]["message"] - if message.annotations["proto-class"] != "message": - continue - break - else: - raise RuntimeError("no candidate for type erasure found") - for cycle in list(cycles): - if node not in cycle: - continue - parent = cycle[cycle.index(node) - 1] - for data in dependency_graph[parent][node].values(): - field = data["field"] - if not quiet: - message_type = dependency_graph.nodes[parent]["message"].base_type - warnings.warn( - f"Type erasing {field.type} {field.name} member in {message_type} to break recursion", - stacklevel=1, - ) - field.annotations["type-erased"] = True - counter.subtract(cycle) - cycles.remove(cycle) diff --git a/proto2ros/proto2ros/descriptors/__init__.py b/proto2ros/proto2ros/descriptors/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/proto2ros/proto2ros/descriptors/sources.py b/proto2ros/proto2ros/descriptors/sources.py deleted file mode 100644 index da30459..0000000 --- a/proto2ros/proto2ros/descriptors/sources.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides APIs to work with Protobuf definition sources.""" - -import functools -import os -from typing import Any, Iterable - -from google.protobuf.descriptor_pb2 import FileDescriptorProto, FileDescriptorSet - - -@functools.singledispatch -def read_source_descriptors(source: Any) -> Iterable[FileDescriptorProto]: - """Reads Protobuf file source descriptors. - - Note this function operates as the entrypoint to all corresponding overloads (via single dispatch). - - Args: - source: a source to be read for descriptors. - - Returns: - an iterable over all file source descriptors found. - """ - raise NotImplementedError(f"not implemented for {source}") - - -@read_source_descriptors.register -def read_source_descriptors_from_bytes(blob: bytes) -> Iterable[FileDescriptorProto]: - """Reads Protobuf file source descriptors from a binary blob. - - Args: - blob: a binary blob, typically read from a .desc file. - - Returns: - an iterable over all file source descriptors found. - """ - descriptor = FileDescriptorSet() - descriptor.ParseFromString(blob) - yield from descriptor.file - - -@read_source_descriptors.register -def read_source_descriptors_from_file(path: os.PathLike) -> Iterable[FileDescriptorProto]: - """Reads Protobuf file source descriptors from binary file. - - Args: - path: path to binary file, typically a .desc file. - - Returns: - an iterable over all file source descriptors found. - """ - with open(path, "rb") as f: - yield from read_source_descriptors_from_bytes(f.read()) diff --git a/proto2ros/proto2ros/descriptors/types.py b/proto2ros/proto2ros/descriptors/types.py deleted file mode 100644 index 2696c2a..0000000 --- a/proto2ros/proto2ros/descriptors/types.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides Protobuf typing details.""" - -from google.protobuf.descriptor_pb2 import FieldDescriptorProto - -COMPOSITE_TYPES = {FieldDescriptorProto.TYPE_MESSAGE, FieldDescriptorProto.TYPE_ENUM} - -PRIMITIVE_TYPE_NAMES = { - FieldDescriptorProto.TYPE_BOOL: "bool", - FieldDescriptorProto.TYPE_DOUBLE: "double", - FieldDescriptorProto.TYPE_FIXED32: "fixed32", - FieldDescriptorProto.TYPE_FIXED64: "fixed64", - FieldDescriptorProto.TYPE_FLOAT: "float", - FieldDescriptorProto.TYPE_INT32: "int32", - FieldDescriptorProto.TYPE_INT64: "int64", - FieldDescriptorProto.TYPE_SFIXED32: "sfixed32", - FieldDescriptorProto.TYPE_SFIXED64: "sfixed64", - FieldDescriptorProto.TYPE_SINT32: "sint32", - FieldDescriptorProto.TYPE_SINT64: "sint64", - FieldDescriptorProto.TYPE_UINT32: "uint32", - FieldDescriptorProto.TYPE_UINT64: "uint64", - FieldDescriptorProto.TYPE_STRING: "string", - FieldDescriptorProto.TYPE_BYTES: "bytes", -} diff --git a/proto2ros/proto2ros/descriptors/utilities.py b/proto2ros/proto2ros/descriptors/utilities.py deleted file mode 100644 index 9d30aab..0000000 --- a/proto2ros/proto2ros/descriptors/utilities.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides utilities to work with Protobuf descriptors. - -Many of these utilities trade in terms of paths and locations. - -Paths are sequences of numbers that refer to an arbitrarily nested value in a Protobuf message. -Each part of a path is one or two numbers: the field number if it is not a repeated field, and -the field number and item index if it is. For example, ``[4, 1, 3]`` can be (but it ultimately -depends on the concrete Protobuf message type) a path to the third item of field number 1 of -the (sub)message that is field number 4 of the message this path applies to. This path can thus -be used to access the target value: take field number 4 of the message, it is a message itself; -then take field number 1 of this (sub)message, it is a repeated field; then take item 3. - -Locations refer to portions of a .proto source file. Locations specify a path to the descriptors -of the constructs that are defined in the corresponding portion. See -[``SourceCodeInfo``](https://github.com/protocolbuffers/protobuf/blob/main/benchmarks/descriptor.proto#L710) -message description for further reference. -""" - -import itertools -from collections.abc import Sequence -from typing import Any, Dict, Iterable, Optional, Tuple - -from google.protobuf.descriptor_pb2 import FieldDescriptorProto, FileDescriptorProto, SourceCodeInfo - -from proto2ros.utilities import identity_lru_cache - - -@identity_lru_cache() -def index_source_code_locations(file_descriptor: FileDescriptorProto) -> Dict[Tuple[int, ...], SourceCodeInfo.Location]: - """Indexes all source code locations in a source file descriptor by path.""" - info = file_descriptor.source_code_info - return {tuple(location.path): location for location in info.location} - - -def walk(proto: Any, path: Sequence[int]) -> Iterable[Any]: - """Iterates a Protobuf message down a given path. - - Args: - proto: a Protobuf message instance to visit. - path: path to iterate along. - - Returns: - an iterable over Protobuf message members. - """ - field_descriptor, field_value = next(item for item in proto.ListFields() if item[0].number == path[0]) - if field_descriptor.label == field_descriptor.LABEL_REPEATED: - field_value = field_value[path[1]] - path = path[1:] - yield field_value - if len(path) > 1: - yield from walk(field_value, path[1:]) - - -def locate_repeated(member: str, proto: Any) -> Iterable[Tuple[Sequence[int], Any]]: - """Iterates over items of a repeated Protobuf message member, also yield their local paths. - - Local paths are tuples of member field number and item index. - - Args: - member: name of the repeated message member field. - proto: Protobuf message instance to access. - - Returns: - an iterable over tuples of local path and member field value. - """ - if member not in proto.DESCRIPTOR.fields_by_name: - raise ValueError(f"{member} is not a member of the given protobuf") - member_field_descriptor = proto.DESCRIPTOR.fields_by_name[member] - if member_field_descriptor.label != FieldDescriptorProto.LABEL_REPEATED: - raise ValueError(f"{member} is not a repeated member of the given protobuf") - for i, member_item in enumerate(getattr(proto, member)): - yield (member_field_descriptor.number, i), member_item - - -def resolve( - source: FileDescriptorProto, - path: Iterable[int], - root: Optional[SourceCodeInfo.Location] = None, -) -> SourceCodeInfo.Location: - """Resolves a source path to a location. - - Args: - source: source file descriptor. - path: source path to be resolved. - root: optional root location to resolve against. - - Returns: - resolved location. - """ - locations = index_source_code_locations(source) - if root is not None: - path = itertools.chain(root.path, path) - path = tuple(path) - if path not in locations: - location = SourceCodeInfo.Location() - location.path.extend(path) - return location - return locations[path] - - -def protofqn(source: FileDescriptorProto, location: SourceCodeInfo.Location) -> str: - """Returns the fully qualified name of a Protobuf composite type. - - This type is to be found at a given `location` in a given `source` file. - """ - name = ".".join(proto.name for proto in walk(source, location.path)) - return f"{source.package}.{name}" diff --git a/proto2ros/proto2ros/equivalences.py b/proto2ros/proto2ros/equivalences.py deleted file mode 100644 index 739dbed..0000000 --- a/proto2ros/proto2ros/equivalences.py +++ /dev/null @@ -1,516 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides APIs to extract Protobuf <-> ROS message equivalences. - -These equivalences are defined in terms of Protobuf composite descriptors -and ROS message specifications. See Protobuf descriptor messages and ROS 2 -``MessageSpecification`` class definition and documentation in -https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto -and https://github.com/ros2/rosidl/blob/rolling/rosidl_adapter/rosidl_adapter/parser.py -respectively for further reference. -""" - -import dataclasses -import functools -import math -from collections.abc import Sequence -from typing import Any, Iterable, List, Optional, Set, Union - -import inflection -from google.protobuf.descriptor_pb2 import ( - DescriptorProto, - EnumDescriptorProto, - FieldDescriptorProto, - FileDescriptorProto, - SourceCodeInfo, -) -from rosidl_adapter.parser import Constant, Field, MessageSpecification, Type - -from proto2ros.configuration import Configuration -from proto2ros.descriptors.types import COMPOSITE_TYPES, PRIMITIVE_TYPE_NAMES -from proto2ros.descriptors.utilities import locate_repeated, protofqn, resolve, walk -from proto2ros.utilities import to_ros_base_type, to_ros_field_name - - -@dataclasses.dataclass -class Equivalence: - """An equivalence relation between a Protobuf composite (message or enum) and a ROS message. - - More than one ROS message specification may be required to represent a single Protobuf - composite. Auxiliary message specifications may be listed in that case. - - Attributes: - proto_spec: a Protobuf composite type descriptor (either a message or an enum). - message_spec: an equivalent ROS message specification. - auxiliary_message_specs: additional ROS message specifications necessary to define - an equivalence (e.g. one-of equivalents, enum equivalents, etc.). - """ - - proto_spec: Union[DescriptorProto, EnumDescriptorProto] - message_spec: MessageSpecification - auxiliary_message_specs: Optional[Sequence[MessageSpecification]] = None - - -def all_message_specifications(eqs: List[Equivalence]) -> Iterable[MessageSpecification]: - """Yields all message specifications to be found in the given equivalence relations.""" - for eq in eqs: - yield eq.message_spec - if eq.auxiliary_message_specs: - yield from eq.auxiliary_message_specs - - -def equivalent_ros_name(source: FileDescriptorProto, location: SourceCodeInfo.Location) -> str: - """Returns an equivalent ROS message name for a Protobuf composite type in a given location.""" - return "".join(inflection.camelize(proto.name) for proto in walk(source, location.path)) - - -def extract_leading_comments(location: SourceCodeInfo.Location) -> str: - """Returns leading comments for construct at the given location.""" - comments = [*location.leading_detached_comments, location.leading_comments] - # Remove backslashes as it causes issues further down the line within rosidl. - comments = [comment.strip("\n").replace("\\", "") for comment in comments] - return "\n\n".join(comment for comment in comments if comment) - - -@functools.singledispatch -def compute_equivalence( - descriptor: Any, - source: FileDescriptorProto, - location: SourceCodeInfo.Location, - config: Configuration, -) -> Equivalence: - """Computes a suitable equivalence relation for some Protobuf composite type. - - Note this function operates as the entrypoint to all corresponding overloads (via single dispatch). - - Args: - descriptor: the descriptor of the Protobuf composite type of interest. - source: the descriptor of the Protobuf source file where the Protobuf composite type is defined. - location: the location of the Protobuf composite type in the aforementioned source file. - config: a suitable configuration for the procedure. - - Returns: - an equivalence relation. - """ - raise NotImplementedError(f"not implemented for {descriptor}") - - -@compute_equivalence.register -def compute_equivalence_for_enum( - descriptor: EnumDescriptorProto, - source: FileDescriptorProto, - location: SourceCodeInfo.Location, - config: Configuration, -) -> Equivalence: - """Computes a suitable equivalence relation for a Protobuf enum type. - - Args: - descriptor: the descriptor of the Protobuf enum of interest. - source: the descriptor of the Protobuf source file where the Protobuf enum type is defined. - location: the location of the Protobuf enum type in the aforementioned source file. - config: a suitable configuration for the procedure. - - Returns: - an equivalence relation. - """ - constants: List[Constant] = [] - for value_path, value_descriptor in locate_repeated("value", descriptor): - constant = Constant("int32", value_descriptor.name.upper(), value_descriptor.number) - value_location = resolve(source, value_path, location) - leading_comments = extract_leading_comments(value_location) - if leading_comments: - constant.annotations["comment"] = leading_comments - constants.append(constant) - fields = [Field(Type("int32"), "value")] - fields[-1].annotations["optional"] = False - package_name = config.package_mapping[source.package] - name = equivalent_ros_name(source, location) - message_spec = MessageSpecification(package_name, name, fields, constants) - leading_comments = extract_leading_comments(location) - if leading_comments: - message_spec.annotations["comment"] = leading_comments - message_spec.annotations["proto-type"] = protofqn(source, location) - message_spec.annotations["proto-class"] = "enum" - return Equivalence(proto_spec=descriptor, message_spec=message_spec) - - -def translate_type_name(name: str, config: Configuration) -> str: - """Translates a Protobuf type name to its ROS equivalent. - - Args: - name: fully qualified Protobuf type name. - config: a suitable configuration for the procedure. - - Returns: - an fully qualified ROS message type name. - - Raises: - ValueError: when `name` is not fully qualified or - when it cannot be resolved to a ROS message type name. - """ - if not name.startswith("."): - raise ValueError(f"'{name}' is not a fully qualified type name") - proto_type_name = name[1:] - - if proto_type_name == "google.protobuf.Any": - return "proto2ros/AnyProto" - - if proto_type_name in config.message_mapping: - return config.message_mapping[proto_type_name] - - matching_proto_packages = [ - proto_package for proto_package in config.package_mapping if proto_type_name.startswith(proto_package + ".") - ] - if matching_proto_packages: - proto_package = max(matching_proto_packages, key=len) - ros_package = config.package_mapping[proto_package] - proto_type_name = proto_type_name.removeprefix(proto_package + ".") - ros_type_name = inflection.camelize(proto_type_name.replace(".", "_")) - return f"{ros_package}/{ros_type_name}" - - if not config.passthrough_unknown: - raise ValueError(f"cannot resolve '{name}' type name") - return "proto2ros/AnyProto" - - -PRIMITIVE_TYPES_MAPPING = { - "bool": "bool", - "double": "float64", - "fixed32": "uint32", - "fixed64": "uint64", - "float": "float32", - "int32": "int32", - "int64": "int64", - "sfixed32": "int32", - "sfixed64": "int64", - "sint32": "int32", - "sint64": "int64", - "uint32": "uint32", - "uint64": "uint64", - "string": "string", -} - - -def translate_type(name: str, repeated: bool, config: Configuration) -> Type: - """Translates a Protobuf type to its ROS equivalent. - - Args: - name: Protobuf type name. - repeated: whether the Protobuf type applies to a repeated field. - config: a suitable configuration for the procedure. - - Returns: - a ROS message type. - """ - if name != "bytes": - if name not in PRIMITIVE_TYPES_MAPPING: - ros_type_name = translate_type_name(name, config) - else: - ros_type_name = PRIMITIVE_TYPES_MAPPING[name] - if repeated: - ros_type_name += "[]" - else: - ros_type_name = "proto2ros/Bytes[]" if repeated else "uint8[]" - return Type(ros_type_name) - - -def translate_any_type(any_expansion: Union[Set[str], str], repeated: bool, config: Configuration) -> Type: - """Translates a ``google.protobuf.Any`` type to its ROS equivalent given an any expansion. - - Args: - any_expansion: a Protobuf message type set that the given ``google.protobuf.Any`` - is expected to pack. A single Protobuf message type may also be specified in lieu - of a single element set. All Protobuf message types must be fully qualified. - repeated: whether the Protobuf type applies to a repeated field. - config: a suitable configuration for the procedure. - - Returns: - a ROS message type. - """ - if config.allow_any_casts and isinstance(any_expansion, str): - # Type name is expected to be fully qualified, thus the leading dot. See - # https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto#L265-L270 - # for further reference. - return translate_type(f".{any_expansion}", repeated, config) - ros_type_name = "proto2ros/Any" - if repeated: - ros_type_name += "[]" - return Type(ros_type_name) - - -def translate_field( - descriptor: FieldDescriptorProto, - source: FileDescriptorProto, - location: SourceCodeInfo.Location, - config: Configuration, -) -> Field: - """Translates a Protobuf field descriptor to its ROS equivalent. - - Args: - descriptor: a Protobuf field descriptor. - source: the descriptor of the Protobuf source file where the Protobuf field is defined. - location: the location of the Protobuf field in the aforementioned source file. - config: a suitable configuration for the procedure. - - Returns: - an fully qualified ROS message type name. - - Raises: - ValueError: when the given field is of an unsupported or unknown type. - ValueError: when an any expansion is specified for a fully typed field. - """ - repeated = descriptor.label == FieldDescriptorProto.LABEL_REPEATED - any_expansion = config.any_expansions.get(protofqn(source, location)) - if any_expansion: - if descriptor.type_name != ".google.protobuf.Any": - raise ValueError(f"any expansion specified for '{descriptor.name}' field of {descriptor.type_name} type") - type_name = descriptor.type_name - - field_type = translate_any_type(any_expansion, repeated, config) - else: - if descriptor.type in PRIMITIVE_TYPE_NAMES: - type_name = PRIMITIVE_TYPE_NAMES[descriptor.type] - elif descriptor.type in COMPOSITE_TYPES: - type_name = descriptor.type_name - else: - raise ValueError(f"unsupported field type: {descriptor.type}") - field_type = translate_type(type_name, repeated, config) - field = Field(field_type, to_ros_field_name(descriptor.name)) - if any_expansion: - if not config.allow_any_casts or not isinstance(any_expansion, str): - # Annotate field with paired Protobuf and ROS message type names, - # so that any expansions can be resolved in conversion code. - field.annotations["type-casts"] = [ - (proto_type, translate_type_name(f".{proto_type}", config)) for proto_type in any_expansion - ] - else: - type_name = f".{any_expansion}" - field.annotations["type-casted"] = True - if source.syntax == "proto3": - field.annotations["optional"] = descriptor.proto3_optional or ( - descriptor.label != FieldDescriptorProto.LABEL_REPEATED - and descriptor.type == FieldDescriptorProto.TYPE_MESSAGE - ) - elif source.syntax == "proto2": - field.annotations["optional"] = descriptor.label != FieldDescriptorProto.LABEL_REPEATED - else: - raise ValueError(f"unknown proto syntax: {source.syntax}") - field.annotations["proto-cpp-name"] = descriptor.name.lower() - field.annotations["proto-py-name"] = descriptor.name - ros_type_name = to_ros_base_type(field_type) - if type_name != ".google.protobuf.Any" and ros_type_name == "proto2ros/AnyProto": - type_name = "some" - field.annotations["proto-type"] = type_name.strip(".") - leading_comments = extract_leading_comments(location) - if leading_comments: - field.annotations["comment"] = leading_comments - field.annotations["deprecated"] = descriptor.options.deprecated - return field - - -@compute_equivalence.register -def compute_equivalence_for_message( - descriptor: DescriptorProto, - source: FileDescriptorProto, - location: SourceCodeInfo.Location, - config: Configuration, -) -> Equivalence: - """Computes a suitable equivalence relation for a Protobuf message type. - - Currently, this function supports optional, repeated, and oneof fields of primitive, enum, - map, message, and `google.protobuf.Any` type. Recursive or cyclic type dependencies may ensue - if these are present in the Protobuf message type (see `proto2ros.dependencies` on how to cope - with this). - - Args: - descriptor: the descriptor of the Protobuf message of interest. - source: the descriptor of the Protobuf source file where the Protobuf message is defined. - location: the location of the Protobuf message in the aforementioned source file. - config: a suitable configuration for the procedure. - - Returns: - an equivalence relation. - - Raises: - ValueError: when there are too many fields (more than 64) and - their availability cannot be encoded in the equivalent ROS message. - """ - ros_package_name = config.package_mapping[source.package] - name = equivalent_ros_name(source, location) - auxiliary_message_specs: List[MessageSpecification] = [] - - fields: List[Field] = [] - constants: List[Constant] = [] - oneof_field_sets: List[List[Field]] = [list() for _ in descriptor.oneof_decl] - - if not descriptor.options.map_entry: - if len(descriptor.field) > 0: - options_mask_size = max(2 ** math.ceil(math.log2(len(descriptor.field))), 8) - if options_mask_size > 64: - raise ValueError("too many fields (> 64)") - options_mask_type = Type(f"uint{options_mask_size}") - - # Iterate over all message fields, as listed by the given descriptor. See - # https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto#L127 - # for further reference. - for i, (field_path, field_descriptor) in enumerate(locate_repeated("field", descriptor)): - if field_descriptor.options.deprecated and config.drop_deprecated: - continue - - field_location = resolve(source, field_path, location) - field = translate_field(field_descriptor, source, field_location, config) - if field_descriptor.HasField("oneof_index"): - oneof_field_sets[field_descriptor.oneof_index].append(field) - continue - - if field.annotations["optional"]: - mask_name = field.name.upper() + "_FIELD_SET" - mask_constant = Constant(str(options_mask_type), mask_name, 1 << i) - constants.append(mask_constant) - fields.append(field) - - for (oneof_path, oneof_decl), oneof_fields in zip( - locate_repeated("oneof_decl", descriptor), - oneof_field_sets, - strict=True, - ): - oneof_name = inflection.underscore(oneof_decl.name) - oneof_type_name = inflection.camelize(f"{name}_one_of_{oneof_name}") - oneof_type = Type(f"{ros_package_name}/{oneof_type_name}") - - oneof_constants: List[Constant] = [] - oneof_constants.append(Constant("int8", f"{oneof_name}_not_set".upper(), 0)) - for i, field in enumerate(oneof_fields, start=1): - tag_name = f"{oneof_name}_{field.name}_set".upper() - oneof_constants.append(Constant("int8", tag_name, i)) - choice_field = Field(Type("int8"), f"{oneof_name}_choice") - choice_field.annotations["deprecated"] = True - oneof_fields.append(choice_field) - which_field = Field(Type("int8"), "which") - which_field.annotations["alias"] = choice_field.name - oneof_fields.append(which_field) - - oneof_message_spec = MessageSpecification( - oneof_type.pkg_name, - oneof_type.type, - oneof_fields, - oneof_constants, - ) - oneof_message_spec.annotations["proto-type"] = protofqn(source, location) + f"[one-of {oneof_decl.name}]" - oneof_message_spec.annotations["proto-class"] = "one-of" - oneof_message_spec.annotations["tagged"] = list(zip(oneof_constants[1:], oneof_fields[:-2], strict=True)) - oneof_message_spec.annotations["tag"] = which_field - auxiliary_message_specs.append(oneof_message_spec) - - field = Field(oneof_type, oneof_name) - # oneof wrapper field cannot itself be optional - field.annotations["optional"] = False - field.annotations["proto-cpp-name"] = oneof_decl.name - field.annotations["proto-py-name"] = oneof_decl.name - oneof_location = resolve(source, oneof_path, location) - leading_comments = extract_leading_comments(oneof_location) - if leading_comments: - field.annotations["comment"] = leading_comments - fields.append(field) - - if len(constants) > 0: - field = Field(options_mask_type, "has_field", 2**options_mask_size - 1) - field.annotations["optional"] = False # field presence mask must always be present - fields.append(field) - else: - for field_path, field_descriptor in locate_repeated("field", descriptor): - if field_descriptor.options.deprecated and config.drop_deprecated: - continue - field_location = resolve(source, field_path, location) - field = translate_field(field_descriptor, source, field_location, config) - field.annotations["optional"] = False # map key-value pairs must always be present - if field.name == "value": - map_inplace = field_descriptor.type == FieldDescriptorProto.TYPE_MESSAGE - fields.append(field) - - message_spec = MessageSpecification(ros_package_name, name, fields, constants) - leading_comments = extract_leading_comments(location) - if leading_comments: - message_spec.annotations["comment"] = leading_comments - message_spec.annotations["proto-type"] = protofqn(source, location) - message_spec.annotations["proto-class"] = "message" - message_spec.annotations["has-optionals"] = len(constants) > 0 - message_spec.annotations["map-entry"] = descriptor.options.map_entry - if descriptor.options.map_entry: - message_spec.annotations["map-inplace"] = map_inplace - for spec in auxiliary_message_specs: - spec.annotations["parent-spec"] = message_spec - return Equivalence( - proto_spec=descriptor, - message_spec=message_spec, - auxiliary_message_specs=auxiliary_message_specs, - ) - - -@functools.singledispatch -def extract_equivalences(descriptor: Any, *args: Any) -> Iterable[Equivalence]: - """Extracts equivalence relations for all Protobuf composite types in some Protobuf descriptor. - - Note this function operates as the entrypoint to all corresponding overloads (via single dispatch). - - Args: - descriptor: the descriptor bearing Protobuf composite types. - args: place holder for arguments - - Returns: - an iterable over equivalence relations. - """ - raise NotImplementedError(f"not implemented for {descriptor}") - - -@extract_equivalences.register -def extract_equivalences_from_message( - message_descriptor: DescriptorProto, - source_descriptor: FileDescriptorProto, - location: SourceCodeInfo.Location, - config: Configuration, -) -> Iterable[Equivalence]: - """Extracts equivalence relations for a Protobuf message type and all nested composite types (if any). - - Args: - message_descriptor: the descriptor of the Protobuf message of interest. - source_descriptor: the descriptor of the Protobuf source file where the Protobuf message is defined. - location: the location of the Protobuf message in the aforementioned source file. - config: a suitable configuration for the procedure. - - Returns: - an iterable over equivalence relations. - """ - yield compute_equivalence_for_message(message_descriptor, source_descriptor, location, config) - for enum_path, enum_descriptor in locate_repeated("enum_type", message_descriptor): - enum_location = resolve(source_descriptor, enum_path, location) - yield compute_equivalence_for_enum(enum_descriptor, source_descriptor, enum_location, config) - for nested_path, nested_descriptor in locate_repeated("nested_type", message_descriptor): - nested_location = resolve(source_descriptor, nested_path, location) - if protofqn(source_descriptor, nested_location) in config.message_mapping: - continue # skip mapped messages - yield from extract_equivalences_from_message(nested_descriptor, source_descriptor, nested_location, config) - - -@extract_equivalences.register -def extract_equivalences_from_source( - source_descriptor: FileDescriptorProto, - config: Configuration, -) -> Iterable[Equivalence]: - """Extracts all equivalence relations from a Protobuf source descriptor. - - Args: - source_descriptor: the descriptor of the Protobuf source file. - config: a suitable configuration for the procedure. - - Returns: - an iterable over equivalence relations. - """ - for enum_path, enum_type in locate_repeated("enum_type", source_descriptor): - enum_location = resolve(source_descriptor, enum_path) - yield compute_equivalence_for_enum(enum_type, source_descriptor, enum_location, config) - for message_path, message_type in locate_repeated("message_type", source_descriptor): - message_location = resolve(source_descriptor, message_path) - if protofqn(source_descriptor, message_location) in config.message_mapping: - continue # skip mapped messages - yield from extract_equivalences_from_message(message_type, source_descriptor, message_location, config) diff --git a/proto2ros/proto2ros/output/__init__.py b/proto2ros/proto2ros/output/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/proto2ros/proto2ros/output/cpp.py b/proto2ros/proto2ros/output/cpp.py deleted file mode 100644 index a9bdeab..0000000 --- a/proto2ros/proto2ros/output/cpp.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) 2024 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides APIs to write C++ conversion code.""" - -import os -import re -from typing import List, Optional, Tuple, Union - -import inflection -import jinja2 -from rosidl_adapter.parser import BaseType, MessageSpecification - -from proto2ros.configuration import Configuration -from proto2ros.utilities import rreplace, to_protobuf_field_name, to_ros_base_type - - -def to_pb2_cpp_header(source_path: os.PathLike) -> str: - """Returns the C++ header for a given Protobuf source file.""" - basename, _ = os.path.splitext(source_path) - return basename + ".pb.h" - - -def itemize_cpp_identifier(identifier: str, prefix: Optional[str] = None) -> str: - """Derives a loop variable identifier for its iterable variable identifier.""" - basename = identifier.rpartition(".")[-1] - basename = basename.rpartition("->")[-1] - basename = basename.partition("(")[0] - if prefix and not basename.startswith(prefix): - basename = prefix + basename - if not basename.endswith("item"): - return f"{basename}_item" - return basename.removesuffix("item") + "subitem" - - -def to_ros_cpp_header(spec: MessageSpecification) -> str: - """Returns the C++ header for a given ROS message specification.""" - basename = inflection.underscore(spec.base_type.type) - return f"{spec.base_type.pkg_name}/msg/{basename}.hpp" - - -def to_ros_cpp_namespace(package_name: str) -> str: - """Returns the C++ namespace for a given ROS message package.""" - return f"{package_name}::msg" - - -def to_ros_cpp_type(type_name: Union[str, BaseType]) -> str: - """Returns the C++ class name for a given ROS message type.""" - package_name, _, name = str(type_name).rpartition("/") - if not package_name: - raise ValueError(f"no package name in {type_name}") - if not name: - raise ValueError(f"no message name in {type_name}") - return f"{to_ros_cpp_namespace(package_name)}::{name}" - - -def to_pb2_cpp_type(type_name: str) -> str: - """Returns the C++ class name for a given Protobuf message type.""" - return type_name.replace(".", "::") - - -def to_hungarian_notation(name: str) -> str: - """Transforms the given valid C++ name to hungarian notation. - - E.g. some_name_of_mine translates to kSomeNameOfMine. - """ - return "k" + inflection.camelize(re.sub(r"([0-9])([a-z])", r"\1_\2", name)) - - -def dump_conversions_cpp_sources( - package_name: str, - message_specifications: List[MessageSpecification], - known_message_specifications: List[MessageSpecification], - config: Configuration, -) -> Tuple[str, str]: - """Dumps the C++ sources for Protobuf <-> ROS conversion APIs. - - Args: - package_name: name of the package that will host the APIs. - message_specifications: annotated ROS message specifications, - as derived from equivalence relations (see `proto2ros.equivalences`). - known_message_specifications: all annotated ROS message specifications known, - including those from dependencies. A superset of ``message_specifications``. - config: a suitable configuration for the procedure. - - Returns: - the conversion C++ header and source files' content, in that order. - """ - env = jinja2.Environment(loader=jinja2.PackageLoader("proto2ros.output")) - env.globals["to_pb2_cpp_name"] = to_protobuf_field_name - env.globals["itemize_cpp_identifier"] = itemize_cpp_identifier - env.globals["to_ros_base_type"] = to_ros_base_type - env.globals["rreplace"] = rreplace - env.filters["as_ros_base_type"] = to_ros_base_type - env.filters["as_ros_cpp_type"] = to_ros_cpp_type - env.filters["as_pb2_cpp_type"] = to_pb2_cpp_type - env.filters["as_pb2_cpp_name"] = to_protobuf_field_name - env.filters["to_hungarian_notation"] = to_hungarian_notation - env.filters["rreplace"] = rreplace - cpp_conversions_header_template = env.get_template("conversions.hpp.jinja") - cpp_conversions_header_content = cpp_conversions_header_template.render( - package_name=package_name, - message_specifications=message_specifications, - config=config, - ) - cpp_conversions_source_template = env.get_template("conversions.cpp.jinja") - cpp_conversions_source_content = cpp_conversions_source_template.render( - package_name=package_name, - message_specifications=message_specifications, - known_message_specifications={str(spec.base_type): spec for spec in known_message_specifications}, - config=config, - ) - return cpp_conversions_header_content, cpp_conversions_source_content diff --git a/proto2ros/proto2ros/output/interfaces.py b/proto2ros/proto2ros/output/interfaces.py deleted file mode 100644 index 12c39c5..0000000 --- a/proto2ros/proto2ros/output/interfaces.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides APIs to write ROS message specifications.""" - -import os -import pathlib -from typing import List, Optional, Union - -from rosidl_adapter.parser import BaseType, Field, MessageSpecification, Type - - -def dump_as_comments(content: str) -> str: - """Dumps the given content as ROS message comment.""" - return "\n".join("#" + line for line in content.splitlines()) - - -def dump_message_specification(message_spec: MessageSpecification) -> str: - """Dumps a ROS message specification back to a string.""" - output = [] - if "comment" in message_spec.annotations: - comment = message_spec.annotations["comment"] - output.append(dump_as_comments(comment)) - if message_spec.constants: - output.append("") # blank line - for constant in message_spec.constants: - if "comment" in constant.annotations: - comment = constant.annotations["comment"] - output.append(dump_as_comments(comment)) - output.append(str(constant)) - if message_spec.fields: - output.append("") # blank line - for field in message_spec.fields: - qualifiers: List[str] = [] - if "comment" in field.annotations: - comment = field.annotations["comment"] - output.append(dump_as_comments(comment)) - if field.annotations.get("deprecated"): - qualifiers.append("deprecated") - if field.annotations.get("type-erased"): - qualifiers.append(f"is {field.type} (type-erased)") - any_type = Type(str(field.type).replace(BaseType.__str__(field.type), "proto2ros/Any")) - field = Field(any_type, field.name) - line = str(field) - if qualifiers: - line += " # " + ", ".join(qualifiers) - output.append(line) - return "\n".join(output) - - -def which_message_specification( - message_spec: MessageSpecification, - root: Optional[Union[str, os.PathLike[str]]] = None, -) -> pathlib.Path: - """Returns an .msg file path for a given ROS message specification. - - ROS .msg file name conversions are observed in the process. - - Args: - message_spec: source ROS message specification. - root: optional root directory for .msg file. - - Returns: - the full .msg file path. - """ - if root is None: - root = "." - return pathlib.Path(root) / f"{message_spec.msg_name}.msg" diff --git a/proto2ros/proto2ros/output/python.py b/proto2ros/proto2ros/output/python.py deleted file mode 100644 index f07bc84..0000000 --- a/proto2ros/proto2ros/output/python.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides APIs to write Python conversion code.""" - -import importlib -import os -import pickle -import sys -from typing import Any, Dict, Iterable, List, Optional, Union - -import inflection -import jinja2 -from google.protobuf.descriptor import Descriptor -from rosidl_adapter.parser import BaseType, MessageSpecification - -from proto2ros.configuration import Configuration -from proto2ros.utilities import fqn, to_ros_base_type - - -def to_python_module_name(module_path: os.PathLike) -> str: - """Returns a Python module name given its source file path.""" - basename, _ = os.path.splitext(module_path) - return basename.replace("/", ".") - - -def to_python_identifier(string: str) -> str: - """Transforms an arbitrary string into a valid Python identifier.""" - denormalized_identifier = string.replace("/", "_").replace(".", "_") - denormalized_identifier = denormalized_identifier.replace("UInt", "uint") - return inflection.underscore(denormalized_identifier) - - -def unqualify_python_identifier(identifier: str) -> str: - """Extracts a Python identifier basename.""" - return identifier.rpartition(".")[-1] - - -def itemize_python_identifier(identifier: str, prefix: Optional[str] = None) -> str: - """Derives a loop variable identifier for its iterable variable identifier.""" - basename = unqualify_python_identifier(identifier) - if prefix and not basename.startswith(prefix): - basename = prefix + basename - if not basename.endswith("item"): - return f"{basename}_item" - return basename.removesuffix("item") + "subitem" - - -def to_ros_python_module_name(package_name: str) -> str: - """Returns the Python module name for a given ROS message package.""" - return f"{package_name}.msg" - - -def to_ros_python_type(type_name: Union[str, BaseType]) -> str: - """Returns the Python class name for a given ROS message type.""" - package_name, _, name = str(type_name).rpartition("/") - if not package_name: - raise ValueError(f"no package name in {type_name}") - if not name: - raise ValueError(f"no message name in {type_name}") - return f"{to_ros_python_module_name(package_name)}.{name}" - - -def to_pb2_python_module_name(source_path: os.PathLike) -> str: - """Returns the Python module name for a given Protobuf source file.""" - basename, _ = os.path.splitext(source_path) - return to_python_module_name(basename + "_pb2.py") - - -def build_pb2_python_type_lut(module_names: Iterable[str], inline_module_names: Iterable[str]) -> Dict[str, str]: - """Builds a lookup table to go from Protobuf message type names to Python class name. - - For instance, if `google.protobuf.any_pb2` is listed as an imported module name and - `google.protobuf.timestamp_pb2` is listed as an inline imported module name, the - resulting lookup table will include entries such as: - - ``{"google.protobuf.Any": "google.protobuf.any_pb2.Any", "google.protobuf.timestamp_pb2.Timestamp": "Timestamp"}`` - - where Python types for Protobuf messages coming from inline imports are unqualified. - - Args: - module_names: names of Python modules to traverse in order to map fully qualified - Protobuf message type names to fully qualified Python class names (assuming these - modules will be imported like ``import ``). - inline_module_names: names of Python modules to traverse in order to map fully - qualified Protobuf message type names to unqualified Python class names (assuming - these modules will be imported like ``from import *``). - - Returns: - a lookup table as a simple dict. - """ - pb2_python_type_lut: Dict[str, str] = {} - module_names = {name for name in module_names if name.endswith("_pb2")} - inline_module_names = {name for name in inline_module_names if name.endswith("_pb2")} - for module_name in (*module_names, *inline_module_names): - module = importlib.import_module(module_name) - attributes = [ - (fqn(value), value) if module_name in module_names else (name, value) - for name, value in module.__dict__.items() - if hasattr(value, "DESCRIPTOR") - ] - while attributes: - name, value = attributes.pop(0) - if not isinstance(value.DESCRIPTOR, Descriptor): - continue - assert name is not None - proto_name = value.DESCRIPTOR.full_name - pb2_python_type_lut[proto_name] = name - attributes.extend( - (f"{name}.{nested_name}", nested_value) - for nested_name, nested_value in value.__dict__.items() - if hasattr(nested_value, "DESCRIPTOR") - ) - del sys.modules[module.__name__] - return pb2_python_type_lut - - -def dump_conversions_python_module( - message_specifications: List[MessageSpecification], - known_message_specifications: List[MessageSpecification], - config: Configuration, -) -> str: - """Dumps the Python module source for Protobuf <-> ROS conversion APIs. - - Args: - message_specifications: annotated ROS message specifications, - as derived from equivalence relations (see `proto2ros.equivalences`). - known_message_specifications: all annotated ROS message specifications known, - including those from dependencies. A superset of ``message_specifications``. - config: a suitable configuration for the procedure. - - Returns: - the conversion Python module source. - """ - env = jinja2.Environment(loader=jinja2.PackageLoader("proto2ros.output")) - env.globals["to_ros_base_type"] = to_ros_base_type - env.globals["itemize_python_identifier"] = itemize_python_identifier - env.filters["as_python_identifier"] = to_python_identifier - env.filters["python_identifier_name"] = unqualify_python_identifier - env.filters["as_ros_python_type"] = to_ros_python_type - pb2_python_type_lut = build_pb2_python_type_lut(config.python_imports, config.inline_python_imports) - - def lookup_pb2_python_type(type_name: str) -> str: - return pb2_python_type_lut[type_name] - - env.filters["as_pb2_python_type"] = lookup_pb2_python_type - env.filters["as_ros_base_type"] = to_ros_base_type - python_conversions_template = env.get_template("conversions.py.jinja") - return python_conversions_template.render( - message_specifications=message_specifications, - known_message_specifications={str(spec.base_type): spec for spec in known_message_specifications}, - config=config, - ) - - -def dump_specifications_python_module(message_specifications: List[MessageSpecification], config: Configuration) -> str: - """Dumps the Python module source bearing message specifications. - - This is the mechanism that enables proto2ros generated packages to depend on other proto2ros generated packages. - - Args: - message_specifications: annotated ROS message specifications, - as derived from equivalence relations (see `proto2ros.equivalences`). - config: a suitable configuration for the procedure. - - Returns: - the specifications Python module source. - """ - env = jinja2.Environment(loader=jinja2.PackageLoader("proto2ros.output")) - - def as_pickle_dump(obj: Any) -> str: - return repr(pickle.dumps(obj)) - - env.filters["as_pickle_dump"] = as_pickle_dump - python_specifications_template = env.get_template("specifications.py.jinja") - return python_specifications_template.render(message_specifications=message_specifications, config=config) diff --git a/proto2ros/proto2ros/output/templates/__init__.py b/proto2ros/proto2ros/output/templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/proto2ros/proto2ros/output/templates/conversions.cpp.jinja b/proto2ros/proto2ros/output/templates/conversions.cpp.jinja deleted file mode 100644 index 86993ba..0000000 --- a/proto2ros/proto2ros/output/templates/conversions.cpp.jinja +++ /dev/null @@ -1,399 +0,0 @@ -// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -// See conversions.cpp.jinja template in the proto2ros.output.templates Python module. - -#include "{{ package_name }}/conversions.hpp" - -#include -#include -#include -#include - -#include -#include - -#include -#include - -#include - -{#- - Expands to code for ROS to Protobuf field conversion. - - Args: - source: identifier of the ROS message field to convert from. - destination: identifier of the Protobuf message field to convert to. - spec: annotated ROS message field specification. --#} -{%- macro ros_to_proto_field_code(source, destination, spec) -%} - {%- if not spec.type.is_primitive_type() -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) -%} - {%- if type_spec and type_spec.annotations.get("proto-class") == "enum" -%} - {#- Handle enum case i.e. extract integral values from ROS wrapper messages. -#} - {%- if spec.type.is_array -%} -{{ destination }}->Reserve(static_cast({{ source }}.size())); -for (const auto& item : {{ source }}) { - {{ destination }}->Add(static_cast<{{ type_spec.annotations["proto-type"] | as_pb2_cpp_type }}>(item.value)); -} - {%- else -%} -{{ destination | rreplace("mutable", "set", 1) | rreplace("()", "", 1) }}( - static_cast<{{ type_spec.annotations["proto-type"] | as_pb2_cpp_type }}>({{ source }}.value)); - {%- endif -%} - {%- elif spec.type.is_array -%} - {%- set input_item = itemize_cpp_identifier(source, "input_") -%} - {%- set output_item = itemize_cpp_identifier(destination, "output_") -%} - {%- if type_spec and type_spec.annotations.get("map-entry") -%}{#- Handle map case. -#} -{{ destination }}->clear(); -for (const auto& {{ input_item }} : {{ source }}) { - auto* {{ output_item }} = &(*{{ destination }})[{{ input_item }}.key]; - {% for field_spec in type_spec.fields if field_spec.name == "value" %} - {%- if not field_spec.type.is_primitive_type() -%} - {%- set field_type_spec = known_message_specifications.get(to_ros_base_type(field_spec.type)) -%} - {%- if field_type_spec and field_type_spec.annotations.get("proto-class") == "enum" -%} - *{{ output_item }} = static_cast<{{ field_type_spec.annotations["proto-type"] | as_pb2_cpp_type }}>({{ input_item }}.value.value); - {%- else -%}{#- Handle message case. -#} - {{ ros_to_proto_composite_field_code_block(input_item + ".value", output_item, field_spec) | indent(4) }} - {%- endif -%} - {%- elif field_spec.annotations["proto-type"] == "bytes" -%} - {#- Handle bytes case. -#} - {{ output_item }}->clear(); - {{ output_item }}->reserve({{ input_item }}.value.size()); - {{ output_item }}->assign({{ input_item }}.value.begin(), {{ input_item }}.value.end()); - {%- else -%}{#- Handle primitive types case. -#} - *{{ output_item }} = {{ input_item }}.value; - {%- endif -%} - {%- endfor %} -} - {%- else -%}{#- Handle sequence case i.e. just convert sequence items. -#} -{{ destination }}->Clear(); -{{ destination }}->Reserve(static_cast({{ source }}.size())); -for (const auto& {{ input_item }} : {{ source }}) { - auto* {{ output_item }} = {{ destination }}->Add(); - {{ ros_to_proto_composite_field_code_block(input_item, output_item, spec) | indent(4) }} -} - {%- endif -%} - {%- else -%}{#- Handle message case. -#} -{{ ros_to_proto_composite_field_code_block(source, destination, spec) }} - {%- endif -%} - {%- elif spec.annotations["proto-type"] == "bytes" -%} - {#- Handle bytes case. -#} - {%- if to_ros_base_type(spec.type) == "proto2ros/Bytes" -%} -{{ destination }}->Clear(); -{{ destination }}->Reserve(static_cast({{ source }}.size())); -for (const auto& blob : {{ source }}) { - auto* value = {{ destination }}->Add(); - value->reserve(blob.data.size()); - value->assign(blob.data.begin(), blob.data.end()); -} - {%- else -%} -{{ destination }}->clear(); -{{ destination }}->reserve({{ source }}.size()); -{{ destination }}->assign({{ source }}.begin(), {{ source }}.end()); - {%- endif -%} - {%- else -%}{#- Handle primitive types case. -#} - {%- if spec.type.is_array -%} -{{ destination }}->Clear(); -{{ destination }}->Reserve(static_cast({{ source }}.size())); -for (auto item : {{ source }}) { - {{ destination }}->Add(std::move(item)); // NOLINT(performance-move-const-arg) -} - {%- else -%} -{{ destination | rreplace("mutable", "set", 1) | rreplace("()", "", 1) }}({{ source }}); - {%- endif -%} - {%- endif -%} -{%- endmacro -%} - -{#- - Expands to a code block for ROS to Protobuf composite field conversion. - - Args: - source: identifier for the ROS message field to convert from. - destination: identifier for the Protobuf message field to convert to. - spec: annotated ROS message field specification. - - Note: code block may expand within a for-loop, during a repeated field expansion. --#} -{%- macro ros_to_proto_composite_field_code_block(source, destination, spec) -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) -%} - {%- if spec.annotations.get("type-erased") -%}{#- ROS message must be deserialized for conversion. -#} -{ - if ({{ source }}.type_name != "{{ spec.type | as_ros_base_type }}") { - std::ostringstream message{"expected {{ spec.type | as_ros_base_type }} message for {{ spec.name }} member"}; - message << ", got " << {{ source }}.type_name; - throw std::runtime_error(message.str()); - } - rclcpp::SerializedMessage serialized_message; - serialized_message.reserve({{ source }}.value.size()); - auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - std::copy({{ source }}.value.begin(), {{ source }}.value.end(), rcl_serialized_message.buffer); - rcl_serialized_message.buffer_length = {{ source }}.value.size(); - const auto serializer = rclcpp::Serialization<{{ spec.type | as_ros_base_type | as_ros_cpp_type }}>{}; - auto typed_ros_message = {{ spec.type | as_ros_base_type | as_ros_cpp_type }}(); - serializer.deserialize_message(&serialized_message, &typed_ros_message); - Convert(typed_ros_message, {{ destination }}); -} - {%- elif spec.annotations.get("type-casted") -%} - {#- ROS message must be converted and packed for assignment. -#} -{ - auto typed_proto_message = {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}(); - Convert({{ source }}, &typed_proto_message); - {{ destination }}->PackFrom(typed_proto_message); -} - {%- elif spec.annotations.get("type-casts") -%} - {#- ROS message must be deserialized according to type, then converted, then packed for assignment. -#} - {% for proto_type_name, ros_type_name in spec.annotations["type-casts"] -%} - {%- if loop.first -%} -if ({{ source }}.type_name == "{{ ros_type_name }}") { - {%- else %} -} else if ({{ source }}.type_name == "{{ ros_type_name }}") { - {%- endif %} - rclcpp::SerializedMessage serialized_message; - serialized_message.reserve({{ source }}.value.size()); - auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - std::copy({{ source }}.value.begin(), {{ source }}.value.end(), rcl_serialized_message.buffer); - rcl_serialized_message.buffer_length = {{ source }}.value.size(); - const auto serializer = rclcpp::Serialization<{{ ros_type_name | as_ros_cpp_type }}>{}; - auto typed_ros_message = {{ ros_type_name | as_ros_cpp_type }}(); - serializer.deserialize_message(&serialized_message, &typed_ros_message); - auto typed_proto_message = {{ proto_type_name | as_pb2_cpp_type }}(); - Convert(typed_ros_message, &typed_proto_message); - {{ destination }}->PackFrom(typed_proto_message); - {%- endfor %} -} else { - std::ostringstream message; - message << "unexpected " << {{ source }}.type_name << " in {{ spec.name }} member"; - throw std::runtime_error(message.str()); -} - {%- elif type_spec and type_spec.annotations.get("tagged") -%} - {#- Handle one-of field case i.e. determine and convert the ROS message member that is set. -#} - {%- set tag_field_spec = type_spec.annotations["tag"] -%} -switch ({{ source }}.{{ tag_field_spec.name }} != 0 ? {{ source }}.{{ tag_field_spec.name }} : {{ source }}.{{ tag_field_spec.annotations["alias"] }}) { - {%- for tag_spec, member_spec in type_spec.annotations["tagged"] -%} - {%- set source_member = source + "." + member_spec.name -%} - {%- set destination_member = destination.rpartition("->")[0] + "->mutable_" + to_pb2_cpp_name(member_spec.annotations.get("proto-cpp-name", member_spec.name)) + "()" %} - case {{ type_spec.base_type | string | as_ros_cpp_type }}::{{ tag_spec.name }}: - {{ ros_to_proto_field_code(source_member, destination_member, member_spec) | indent(8) }} - break; - {%- endfor %} - default: - break; -} - {%- else -%}{#- Handle the generic ROS message case (because it is appropriate or because we do not know any better). -#} -Convert({{ source }}, {{ destination }}); - {%- endif -%} -{%- endmacro -%} - -{#- - Expands to code for Protobuf to ROS field conversion. - - Args: - source: identifier of the Protobuf message field to convert from. - destination: identifier of the ROS message field to convert to. - spec: annotated ROS message field specification. --#} -{%- macro proto_to_ros_field_code(source, destination, spec) -%} - {%- if not spec.type.is_primitive_type() -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) %} - {%- if type_spec and type_spec.annotations.get("proto-class") == "enum" -%} - {#- Handle enum case i.e. wrap integral values in ROS messages. -#} - {%- if spec.type.is_array -%} -{{ destination }}.clear(); -{{ destination }}.reserve({{ source }}.size()); -for (const auto& value : {{ source }}) { - auto& mutable_enum_message = {{ destination }}.emplace_back(); - mutable_enum_message.value = static_cast(value); -} - {%- else -%} -{{ destination }}.value = static_cast({{ source }}); - {%- endif -%} - {%- elif spec.type.is_array -%} - {%- set input_item = itemize_cpp_identifier(source, "input_") -%} - {%- set output_item = itemize_cpp_identifier(destination, "output_") -%} - {%- if type_spec and type_spec.annotations.get("map-entry") -%} - {#- Handle map case i.e. reconstruct map entries, the convert, then assign. -#} -for (const auto& {{ input_item }} : {{ source }}) { - auto& {{ output_item }} = {{ destination }}.emplace_back(); - {{ output_item }}.key = {{ input_item }}.first; - {%- for field_spec in type_spec.fields if field_spec.name == "value" %} - {{ proto_to_ros_field_code(input_item + ".second", output_item + ".value", field_spec) | indent(4) }} - {%- endfor %} -} - {%- else -%} - {#- Handle sequence case. -#} -for (const auto& {{ input_item }} : {{ source }}) { - auto &{{ output_item }} = {{ destination }}.emplace_back(); - {{ proto_to_ros_composite_field_code_block(input_item, output_item, spec) | indent(4) }} -} - {%- endif %} - {%- else -%}{#- Handle message case. -#} -{{ proto_to_ros_composite_field_code_block(source, destination, spec) }} - {%- endif -%} - {%- elif spec.annotations["proto-type"] == "bytes" %} - {#- Handle bytes case. -#} - {%- if to_ros_base_type(spec.type) == "proto2ros/Bytes" -%} -{{ destination }}.clear(); -{{ destination }}.reserve({{ source }}.size()); -for (const auto& blob : {{ source }}) { - auto& mutable_message = {{ destination }}.emplace_back(); - mutable_message.data.assign(blob.begin(), blob.end()); -} - {%- else -%} -{{ destination }}.clear(); -{{ destination }}.reserve({{ source }}.size()); -{{ destination }}.assign({{ source }}.begin(), {{ source }}.end()); - {%- endif -%} - {%- else -%}{#- Handle primitive types case. -#} - {%- if spec.type.is_array -%} -{{ destination }}.clear(); -{{ destination }}.reserve({{ source }}.size()); -{{ destination }}.assign({{ source }}.begin(), {{ source }}.end()); - {%- else -%} -{{ destination }} = {{ source }}; - {%- endif -%} - {%- endif -%} -{%- endmacro -%} - -{#- - Expands to a code block for Protobuf to ROS composite field conversion. - - Args: - source: identifier of the Protobuf message field to convert from. - destination: identifier of the ROS message field to convert to. - spec: annotated ROS message field specification. - - Note: code block may expand within a for-loop, during a repeated field expansion. --#} -{%- macro proto_to_ros_composite_field_code_block(source, destination, spec) -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) -%} - {%- if spec.annotations.get("type-erased") -%}{#- ROS message must be serialized for assignment. -#} -{ - auto typed_ros_message = {{ spec.type | as_ros_base_type | as_ros_cpp_type }}(); - Convert({{ source }}, &typed_ros_message); - const auto serde = rclcpp::Serialization<{{ spec.type | as_ros_base_type | as_ros_cpp_type }}>{}; - rclcpp::SerializedMessage serialized_message; - serde.serialize_message(&typed_ros_message, &serialized_message); - const auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - const auto* begin = rcl_serialized_message.buffer; - const auto* end = begin + rcl_serialized_message.buffer_length; - {{ destination }}.value.assign(begin, end); - {{ destination }}.type_name = "{{ spec.type | as_ros_base_type }}"; -} - {%- elif spec.annotations.get("type-casted") -%} - {#- Protobuf message must be unpacked for conversion. -#} -{ - auto typed_message = {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}(); - if (!{{ source }}.UnpackTo(&typed_message)) { - throw std::runtime_error("Failed to unpack protobuf, internal error"); - } - Convert(typed_message, &{{ destination }}); -} - {%- elif spec.annotations.get("type-casts") -%} - {#- Protobuf message must be unpacked according to type, then converted, then serialized for assignment. -#} -{ - {%- for proto_type_name, ros_type_name in spec.annotations["type-casts"] -%} - {%- if loop.first %} - if ({{ source }}.Is<{{ proto_type_name | as_pb2_cpp_type }}>()) { - {%- else %} - } else if ({{ source }}.Is<{{ proto_type_name | as_pb2_cpp_type }}>()) { - {%- endif %} - auto typed_protobuf_message = {{ proto_type_name | as_pb2_cpp_type }}(); - if (!{{ source }}.UnpackTo(&typed_protobuf_message)) { - throw std::runtime_error("Failed to unpack any protobuf, internal error"); - } - auto typed_ros_message = {{ ros_type_name | as_ros_cpp_type }}(); - Convert(typed_protobuf_message, &typed_ros_message); - const auto serializer = rclcpp::Serialization<{{ ros_type_name | as_ros_cpp_type }}>{}; - rclcpp::SerializedMessage serialized_message; - serializer.serialize_message(&typed_ros_message, &serialized_message); - const auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - const auto* begin = rcl_serialized_message.buffer; - const auto* end = begin + rcl_serialized_message.buffer_length; - {{ destination }}.value.assign(begin, end); - {{ destination }}.type_name = "{{ ros_type_name }}"; - {%- endfor %} - } else { - std::ostringstream message{"unknown protobuf message type in {{ spec.name }} member"}; - message << ": " << {{ source }}.type_url(); - throw std::runtime_error(message.str()); - } -} - {%- elif type_spec and type_spec.annotations.get("tagged") -%} - {#- Handle one-of field case i.e. determine and convert the Protobuf message member that is set. -#} - {%- set source_parent = source.rpartition(".")[0] -%} - {%- set source_case = rreplace(source, "()", "_case()", 1) -%}{#- adds case suffix to the last getter. -#} - {%- set tag_field_spec = type_spec.annotations["tag"] -%} -switch ({{ source_case }}) { - {%- for tag_spec, member_spec in type_spec.annotations["tagged"] %} - {%- set parent_type_spec = type_spec.annotations["parent-spec"] -%} - {%- set source_member = source_parent + "." + to_pb2_cpp_name(member_spec.annotations.get("proto-cpp-name", member_spec.name)) + "()" -%} - {%- set destination_member = destination + "." + member_spec.name %} - case {{ parent_type_spec.annotations["proto-type"] | as_pb2_cpp_type }}::{{ member_spec.annotations.get("proto-cpp-name", member_spec.name) | to_hungarian_notation }}: - {{ proto_to_ros_field_code(source_member, destination_member, member_spec) | indent(8) }} - {{ destination }}.{{ tag_field_spec.name }} = {{ type_spec.base_type | string | as_ros_cpp_type }}::{{ tag_spec.name }}; - {{ destination }}.{{ tag_field_spec.annotations["alias"] }} = {{ destination }}.{{ tag_field_spec.name }}; - break; - {%- endfor %} - default: - {{ destination }}.{{ tag_field_spec.name }} = 0; - {{ destination }}.{{ tag_field_spec.annotations["alias"] }} = 0; - break; -} - {%- else -%}{#- Handle the generic Protobuf message case (because it is appropriate or because we do not know any better). -#} -Convert({{ source }}, &{{ destination }}); - {%- endif -%} -{%- endmacro %} - -namespace {{ package_name }}::conversions { - -using proto2ros::conversions::Convert; -{%- for namespace in config.inline_cpp_namespaces %} -using {{ namespace }}::Convert; -{%- endfor %} - -{% for spec in message_specifications if spec.annotations.get("proto-class") == "message" and not spec.annotations.get("map-entry") -%} -void Convert(const {{ spec.base_type | string | as_ros_cpp_type }}& ros_msg, {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}* proto_msg) { - assert(proto_msg != nullptr); - {%- if spec.fields %} - proto_msg->Clear(); - {%- for field_spec in spec.fields if field_spec.name != "has_field" %} - {%- set source = "ros_msg." + field_spec.name -%} - {%- set destination = "proto_msg->mutable_" + to_pb2_cpp_name(field_spec.annotations.get("proto-cpp-name", field_spec.name)) + "()" -%} - {%- if field_spec.annotations["optional"] %}{#- Check for field presence before use. #} - if ((ros_msg.has_field & {{ spec.base_type | string | as_ros_cpp_type }}::{{ field_spec.name | upper }}_FIELD_SET) != 0) { - {{ ros_to_proto_field_code(source, destination, field_spec) | indent(8) }} - } - {%- else %} - {{ ros_to_proto_field_code(source, destination, field_spec) | indent(4) }} - {%- endif -%} - {%- endfor -%} - {% else -%}{#- Handle empty message. #} - std::ignore = ros_msg; - std::ignore = proto_msg; - {%- endif %} -} - -void Convert(const {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}& proto_msg, {{ spec.base_type | string | as_ros_cpp_type }}* ros_msg) { - assert(ros_msg != nullptr); - {%- if spec.fields -%} - {%- if spec.annotations["has-optionals"] %} - ros_msg->has_field = 0U; - {%- endif -%} - {%- for field_spec in spec.fields if field_spec.name != "has_field" -%} - {%- set source = "proto_msg." + to_pb2_cpp_name(field_spec.annotations.get("proto-cpp-name", field_spec.name)) + "()" -%} - {%- set destination = "ros_msg->" + field_spec.name -%} - {%- if field_spec.annotations["optional"] -%}{#- Check for field presence before use. #} - if (proto_msg.has_{{ field_spec.annotations.get("proto-cpp-name", field_spec.name) | as_pb2_cpp_name }}()) { - {{ proto_to_ros_field_code(source, destination, field_spec) | indent(8) }} - ros_msg->has_field |= {{ spec.base_type | string | as_ros_cpp_type }}::{{ field_spec.name | upper }}_FIELD_SET; - } - {%- else %} - {{ proto_to_ros_field_code(source, destination, field_spec) | indent(4) }} - {%- endif -%} - {%- endfor -%} - {% else -%}{#- Handle empty message. #} - std::ignore = proto_msg; - std::ignore = ros_msg; - {%- endif %} -} - -{% endfor -%} -} // namespace {{ package_name }}::conversions diff --git a/proto2ros/proto2ros/output/templates/conversions.hpp.jinja b/proto2ros/proto2ros/output/templates/conversions.hpp.jinja deleted file mode 100644 index 8f08883..0000000 --- a/proto2ros/proto2ros/output/templates/conversions.hpp.jinja +++ /dev/null @@ -1,31 +0,0 @@ -// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -// See conversions.hpp.jinja template in the proto2ros.output.templates Python module. - -#pragma once -{% for header in config.cpp_headers|sort %} -#include <{{ header }}> -{%- endfor %} - -namespace {{ package_name }}::conversions { -{% for spec in message_specifications if spec.annotations.get("proto-class") == "message" and not spec.annotations.get("map-entry") %} -/// Convert from {{ spec.annotations["proto-type"] }} Protobuf messages to {{ spec.base_type }} ROS messages. -void Convert(const {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}& proto_msg, {{ spec.base_type | string | as_ros_cpp_type }}* ros_msg); - -/// Convert from {{ spec.base_type }} ROS messages to {{ spec.annotations["proto-type"] }} Protobuf messages. -void Convert(const {{ spec.base_type | string | as_ros_cpp_type }}& ros_msg, {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}* proto_msg); - -/// Convert from {{ spec.annotations["proto-type"] }} Protobuf messages to {{ spec.base_type }} ROS messages. -inline {{ spec.base_type | string | as_ros_cpp_type }} Convert(const {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}& proto_msg) { - auto ros_msg = {{ spec.base_type | string | as_ros_cpp_type }}(); - Convert(proto_msg, &ros_msg); - return ros_msg; -} - -/// Convert from {{ spec.base_type }} ROS messages to {{ spec.annotations["proto-type"] }} Protobuf messages. -inline {{ spec.annotations["proto-type"] | as_pb2_cpp_type }} Convert(const {{ spec.base_type | string | as_ros_cpp_type }}& ros_msg) { - auto proto_msg = {{ spec.annotations["proto-type"] | as_pb2_cpp_type }}(); - Convert(ros_msg, &proto_msg); - return proto_msg; -} -{% endfor %} -} // namespace {{ package_name }}::conversions diff --git a/proto2ros/proto2ros/output/templates/conversions.py.jinja b/proto2ros/proto2ros/output/templates/conversions.py.jinja deleted file mode 100644 index a06cd35..0000000 --- a/proto2ros/proto2ros/output/templates/conversions.py.jinja +++ /dev/null @@ -1,301 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# See `conversions.py.jinja` template in the `proto2ros.output.templates` Python module. -import rclpy.serialization - -{% for module_name in config.python_imports -%} -import {{ module_name }} -{% endfor %} -from proto2ros.conversions import convert -{% for module_name in config.inline_python_imports -%} -from {{ module_name }} import * -{% endfor -%} - -{# - Expands to code for ROS to Protobuf field conversion. - - Args: - source: identifier of the ROS message field to convert from. - destination: identifier of the Protobuf message field to convert to. - spec: annotated ROS message field specification. -#} -{%- macro ros_to_proto_field_code(source, destination, spec) -%} - {%- if not spec.type.is_primitive_type() -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) -%} - {%- if type_spec and type_spec.annotations.get("proto-class") == "enum" -%} - {#- Handle enum case i.e. extract integral values from ROS wrapper messages. -#} - {%- if spec.type.is_array -%} -{{ destination }}.extend(item.value for item in {{ source }}) - {%- else -%} -{{ destination }} = {{ source }}.value - {%- endif -%} - {%- elif spec.type.is_array -%} - {%- set input_item = itemize_python_identifier(source, "input_") -%} - {%- set output_item = itemize_python_identifier(destination, "output_") -%} - {%- if type_spec and type_spec.annotations.get("map-entry") -%} - {#- Handle map case i.e. convert map entries, then update Protobuf messsage map. -#} -{{ output_item }} = {{ type_spec.annotations["proto-type"] | as_pb2_python_type }}() -for {{ input_item }} in {{ source }}: - {{ ros_to_proto_composite_field_code_block(input_item, output_item, spec) | indent(4) }} - {%- if type_spec.annotations.get("map-inplace") -%} - {#- Then map value cannot be assigned, must be copied. #} - {{ destination }}[{{ output_item }}.key].CopyFrom({{ output_item }}.value) - {%- else %} - {{ destination }}[{{ output_item }}.key] = {{ output_item }}.value - {%- endif %} - {%- else -%}{#- Handle sequence case i.e. just convert sequence items. #} -for {{ input_item }} in {{ source }}: - {{ output_item }} = {{ destination }}.add() - {{ ros_to_proto_composite_field_code_block(input_item, output_item, spec) | indent(4) }} - {%- endif -%} - {%- else -%}{#- Handle message case. -#} -{{ ros_to_proto_composite_field_code_block(source, destination, spec) }} - {%- endif -%} - {%- elif spec.annotations["proto-type"] == "bytes" -%} - {#- Handle bytes case. -#} - {%- if to_ros_base_type(spec.type) == "proto2ros/Bytes" -%} -{{ destination }}.extend(blob.data.tobytes() for blob in {{ source }}) - {%- else -%} -{{ destination }} = {{ source }}.tobytes() - {%- endif -%} - {%- else -%}{#- Handle primitive types case. -#} - {%- if spec.type.is_array -%} -{{ destination }}.extend({{ source }}) - {%- else -%} -{{ destination }} = {{ source }} - {%- endif -%} - {%- endif -%} -{%- endmacro -%} - -{# - Expands to a code block for ROS to Protobuf composite field conversion. - - Args: - source: identifier for the ROS message field to convert from. - destination: identifier for the Protobuf message field to convert to. - spec: annotated ROS message field specification. - - Note: code block may expand within a for-loop, during a repeated field expansion. -#} -{%- macro ros_to_proto_composite_field_code_block(source, destination, spec) -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) -%} - {%- if spec.annotations.get("type-erased") -%} - {#- ROS message must be deserialized for conversion. -#} -if {{ source }}.type_name != "{{ spec.type | as_ros_base_type }}": - raise ValueError("expected {{ spec.type | as_ros_base_type }} message for {{ spec.name }} member, got %s" % {{ source }}.type) -typed_{{ source | python_identifier_name }}_message = rclpy.serialization.deserialize_message({{ source }}.value.tobytes(), {{ spec.type | as_ros_base_type | as_ros_python_type }}) -convert_{{ spec.type | as_ros_base_type | as_python_identifier }}_message_to_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto(typed_{{ source | python_identifier_name }}_message, {{ destination }}) - {%- elif spec.annotations.get("type-casted") -%} - {#- ROS message must be converted and packed for assignment. -#} -typed_{{ source | python_identifier_name }}_message = {{ spec.annotations["proto-type"] | as_pb2_python_type }}() -convert_{{ spec.type | as_ros_base_type | as_python_identifier }}_message_to_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto({{ source }}, typed_{{ source | python_identifier_name }}_message) -{{ destination }}.Pack(typed_{{ source | python_identifier_name }}_message) - {%- elif spec.annotations.get("type-casts") -%} - {#- ROS message must be deserialized according to type, then converted, then packed for assignment. -#} - {% for proto_type_name, ros_type_name in spec.annotations["type-casts"] -%} - {%- if loop.first -%} -if {{ source }}.type_name == "{{ ros_type_name }}": - {%- else %} -elif {{ source }}.type_name == "{{ ros_type_name }}": - {%- endif %} - typed_{{ ros_type_name | as_python_identifier | python_identifier_name }}_message = rclpy.serialization.deserialize_message({{ source }}.value.tobytes(), {{ ros_type_name | as_ros_python_type }}) - typed_{{ proto_type_name | as_python_identifier | python_identifier_name }}_message = {{ proto_type_name | as_pb2_python_type }}() - convert_{{ ros_type_name | as_python_identifier }}_message_to_{{ proto_type_name | as_python_identifier }}_proto(typed_{{ ros_type_name | as_python_identifier | python_identifier_name }}_message, typed_{{ proto_type_name | as_python_identifier | python_identifier_name }}_message) - {{ destination }}.Pack(typed_{{ proto_type_name | as_python_identifier | python_identifier_name }}_message) - {%- endfor %} -else: - raise ValueError("unexpected %s in {{ spec.name }} member:" % ros_msg.{{ spec.name }}.type) - {%- elif type_spec and type_spec.annotations.get("tagged") -%} - {#- Handle one-of field case i.e. determine and convert the ROS message member that is set. -#} - {%- set tag_field_spec = type_spec.annotations["tag"] -%} -which = {{ source }}.{{ tag_field_spec.name }} or {{ source }}.{{ tag_field_spec.annotations["alias"] }} - {%- for tag_spec, member_spec in type_spec.annotations["tagged"] %} - {%- if loop.first %} -if which == {{ type_spec.base_type | string | as_ros_python_type }}.{{ tag_spec.name }}: - {%- else %} -elif which == {{ type_spec.base_type | string | as_ros_python_type }}.{{ tag_spec.name }}: - {%- endif %} - {%- set source_member = source + "." + member_spec.name -%} - {%- set destination_member = destination.rpartition(".")[0] + "." + member_spec.annotations.get("proto-py-name", member_spec.name) %} - {{ ros_to_proto_field_code(source_member, destination_member, member_spec) | indent(4) }} - {%- endfor %} -else: - pass - {%- else -%} - {#- Handle the generic ROS message case (because it is appropriate or because we do not know any better). -#} -convert_{{ spec.type | as_ros_base_type | as_python_identifier }}_message_to_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto({{ source }}, {{ destination }}) - {%- endif -%} -{%- endmacro -%} - -{# - Expands to code for Protobuf to ROS field conversion. - - Args: - source: identifier of the Protobuf message field to convert from. - destination: identifier of the ROS message field to convert to. - spec: annotated ROS message field specification. -#} -{%- macro proto_to_ros_field_code(source, destination, spec) -%} - {%- if not spec.type.is_primitive_type() -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) -%} - {%- if type_spec and type_spec.annotations.get("proto-class") == "enum" -%} - {#- Handle enum case i.e. wrap integral values in ROS messages. -#} - {%- if spec.type.is_array -%} -{{ destination }} = [{{ spec.type | as_ros_base_type | as_ros_python_type }}(value=value) for value in {{ source }}] - {%- else -%} -{{ destination }}.value = {{ source }} - {%- endif -%} - {%- elif spec.type.is_array -%} - {%- set input_item = itemize_python_identifier(source, "input_") -%} - {%- set output_item = itemize_python_identifier(destination, "output_") -%} - {%- if type_spec and type_spec.annotations.get("map-entry") %} - {#- Handle map case i.e. reconstruct map entries, the convert, then assign. -#} - {%- set input_iterable = input_item + "_entries" -%} -{{ input_iterable }} = [{{ type_spec.annotations["proto-type"] | as_pb2_python_type }}(key=key, value=value) for key, value in {{ source }}.items()] - {%- else -%} - {#- Handle sequence case. -#} - {%- set input_iterable = source -%} - {%- endif %} -for {{ input_item }} in {{ input_iterable }}: - {%- if not spec.annotations.get("type-erased") %} - {{ output_item }} = {{ spec.type | as_ros_base_type | as_ros_python_type }}() - {%- else %}{#- It must convert to a type erased ROS message. #} - {{ output_item }} = proto2ros.msg.Any() - {%- endif %} - {{ proto_to_ros_composite_field_code_block(input_item, output_item, spec) | indent(4) }} - {{ destination }}.append({{ output_item }}) - {%- else -%}{#- Handle message case. -#} -{{ proto_to_ros_composite_field_code_block(source, destination, spec) }} - {%- endif -%} - {%- elif spec.annotations["proto-type"] == "bytes" -%} - {#- Handle bytes case. -#} - {%- if to_ros_base_type(spec.type) == "proto2ros/Bytes" -%} -{{ destination }} = [proto2ros.msg.Bytes(data=blob) for blob in {{ source }}] - {%- else -%} -{{ destination }} = {{ source }} - {%- endif -%} - {%- else -%}{#- Handle primitive types case. -#} -{{ destination }} = {{ source }} - {%- endif -%} -{%- endmacro -%} - -{# - Expands to a code block for Protobuf to ROS composite field conversion. - - Args: - source: identifier of the Protobuf message field to convert from. - destination: identifier of the ROS message field to convert to. - spec: annotated ROS message field specification. - - Note: code block may expand within a for-loop, during a repeated field expansion. -#} -{%- macro proto_to_ros_composite_field_code_block(source, destination, spec) -%} - {%- set type_spec = known_message_specifications.get(to_ros_base_type(spec.type)) -%} - {%- if spec.annotations.get("type-erased") -%} - {#- ROS message must be serialized for assignment. -#} -typed_{{ source | python_identifier_name }}_message = {{ spec.type | as_ros_base_type | as_ros_python_type }}() -convert_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto_to_{{ spec.type | as_ros_base_type | as_python_identifier }}_message({{ source }}, typed_{{ source | python_identifier_name }}_message) -{{ destination }}.value = rclpy.serialization.serialize_message(typed_{{ source | python_identifier_name }}_message) -{{ destination }}.type_name = "{{ spec.type | as_ros_base_type }}" - {%- elif spec.annotations.get("type-casted") -%} - {#- Protobuf message must be unpacked for conversion. -#} -typed_{{ source | python_identifier_name }}_message = {{ spec.annotations["proto-type"] | as_pb2_python_type }}() -{{ source }}.Unpack(typed_{{ source | python_identifier_name }}_message) -convert_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto_to_{{ spec.type | as_ros_base_type | as_python_identifier }}_message(typed_{{ source | python_identifier_name }}_message, {{ destination }}) - {%- elif spec.annotations.get("type-casts") -%} - {#- Protobuf message must be unpacked according to type, then converted, then serialized for assignment. -#} - {%- for proto_type_name, ros_type_name in spec.annotations["type-casts"] -%} - {%- if loop.first -%} -if {{ source }}.Is({{ proto_type_name | as_pb2_python_type }}.DESCRIPTOR): - {%- else %} -elif {{ source }}.Is({{ proto_type_name | as_pb2_python_type }}.DESCRIPTOR): - {%- endif %} - typed_{{ proto_type_name | as_python_identifier | python_identifier_name }}_message = {{ proto_type_name | as_pb2_python_type }}() - ok = {{ source }}.Unpack(typed_{{ proto_type_name | as_python_identifier | python_identifier_name }}_message) - assert ok, "Failed to unpack any protobuf, internal error" - typed_{{ ros_type_name | as_python_identifier | python_identifier_name }}_message = {{ ros_type_name | as_ros_python_type }}() - convert_{{ proto_type_name | as_python_identifier }}_proto_to_{{ ros_type_name | as_python_identifier }}_message(typed_{{ proto_type_name | as_python_identifier | python_identifier_name }}_message, typed_{{ ros_type_name | as_python_identifier | python_identifier_name }}_message) - {{ destination }}.value = rclpy.serialization.serialize_message(typed_{{ ros_type_name | as_python_identifier | python_identifier_name }}_message) - {{ destination }}.type_name = "{{ ros_type_name }}" - {%- endfor %} -else: - raise ValueError("unknown protobuf message type in {{ spec.name }} member: %s" % {{ source }}.type_url) - {%- elif type_spec and type_spec.annotations.get("tagged") -%} - {#- Handle one-of field case i.e. determine and convert the Protobuf message member that is set. -#} -which = {{ source.rpartition(".")[0] }}.WhichOneof("{{ spec.annotations.get("proto-py-name", spec.name) }}") - {%- set tag_field_spec = type_spec.annotations["tag"] %} - {%- for tag_spec, member_spec in type_spec.annotations["tagged"] %} - {%- if loop.first %} -if which == "{{ member_spec.name }}": - {%- else %} -elif which == "{{ member_spec.name }}": - {%- endif %} - {%- set source_member = source.rpartition(".")[0] + "." + member_spec.annotations.get("proto-py-name", member_spec.name) -%} - {%- set destination_member = destination + "." + member_spec.name %} - {{ proto_to_ros_field_code(source_member, destination_member, member_spec) | indent(4) }} - {{ destination }}.{{ tag_field_spec.name }} = {{ type_spec.base_type | string | as_ros_python_type }}.{{ tag_spec.name }} - {{ destination }}.{{ tag_field_spec.annotations["alias"] }} = {{ destination }}.{{ tag_field_spec.name }} - {%- endfor %} -else: - {{ destination }}.{{ tag_field_spec.name }} = 0 - {{ destination }}.{{ tag_field_spec.annotations["alias"] }} = 0 - {%- else -%} - {#- Handle the generic Protobuf message case (because it is appropriate or because we do not know any better). -#} -convert_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto_to_{{ spec.type | as_ros_base_type | as_python_identifier }}_message({{ source }}, {{ destination }}) - {%- endif -%} -{%- endmacro %} -{% for spec in message_specifications if spec.annotations.get("proto-class") == "message" %} -@convert.register({{ spec.base_type | string | as_ros_python_type }}, {{ spec.annotations["proto-type"] | as_pb2_python_type }}) -def convert_{{ spec.base_type | string | as_python_identifier }}_message_to_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto( - ros_msg: {{ spec.base_type | string | as_ros_python_type }}, proto_msg: {{ spec.annotations["proto-type"] | as_pb2_python_type }} -) -> None: - """Convert from {{ spec.base_type }} ROS messages to {{ spec.annotations["proto-type"] }} Protobuf messages.""" - {%- if spec.fields %} - proto_msg.Clear() - {%- for field_spec in spec.fields if field_spec.name != "has_field" %} - {%- set source = "ros_msg." + field_spec.name -%} - {%- set destination = "proto_msg." + field_spec.annotations.get("proto-py-name", field_spec.name) -%} - {%- if field_spec.annotations["optional"] %}{#- Check for field presence before use. #} - if ros_msg.has_field & {{ spec.base_type | string | as_ros_python_type }}.{{ field_spec.name | upper }}_FIELD_SET: - {{ ros_to_proto_field_code(source, destination, field_spec) | indent(8) }} - {%- else %} - {{ ros_to_proto_field_code(source, destination, field_spec) | indent(4) }} - {%- endif -%} - {%- endfor -%} - {% else -%}{#- Handle empty message. #} - proto_msg.SetInParent() - {%- endif %} - - -convert_{{ spec.base_type | string | as_python_identifier }}_to_proto = \ - convert_{{ spec.base_type | string | as_python_identifier }}_message_to_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto - - -@convert.register({{ spec.annotations["proto-type"] | as_pb2_python_type }}, {{ spec.base_type | string | as_ros_python_type }}) -def convert_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto_to_{{ spec.base_type | string | as_python_identifier }}_message( - proto_msg: {{ spec.annotations["proto-type"] | as_pb2_python_type }}, ros_msg: {{ spec.base_type | string | as_ros_python_type }} -) -> None: - """Convert from {{ spec.annotations["proto-type"] }} Protobuf messages to {{ spec.base_type }} ROS messages.""" - {%- if spec.fields %} - {%- if spec.annotations["has-optionals"] %} - ros_msg.has_field = 0 - {%- endif -%} - {%- for field_spec in spec.fields if field_spec.name != "has_field" -%} - {%- set source = "proto_msg." + field_spec.annotations.get("proto-py-name", field_spec.name) -%} - {%- set destination = "ros_msg." + field_spec.name -%} - {%- if field_spec.annotations["optional"] -%}{#- Check for field presence before use. #} - if proto_msg.HasField("{{ field_spec.annotations.get("proto-py-name", field_spec.name) }}"): - {{ proto_to_ros_field_code(source, destination, field_spec) | indent(8) }} - ros_msg.has_field |= {{ spec.base_type | string | as_ros_python_type }}.{{ field_spec.name | upper }}_FIELD_SET - {%- else %} - {{ proto_to_ros_field_code(source, destination, field_spec) | indent(4) }} - {%- endif -%} - {%- endfor -%} - {% else -%}{#- Handle empty message. #} - pass - {%- endif %} - - -convert_proto_to_{{ spec.base_type | string | as_python_identifier }} = \ - convert_{{ spec.annotations["proto-type"] | as_python_identifier }}_proto_to_{{ spec.base_type | string | as_python_identifier }}_message - -{% endfor -%} diff --git a/proto2ros/proto2ros/output/templates/specifications.py.jinja b/proto2ros/proto2ros/output/templates/specifications.py.jinja deleted file mode 100644 index 406968f..0000000 --- a/proto2ros/proto2ros/output/templates/specifications.py.jinja +++ /dev/null @@ -1,6 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# See `specifications.py.jinja` template in the `proto2ros.output.templates` Python module. - -import pickle - -messages = pickle.loads({{ message_specifications | as_pickle_dump }}) diff --git a/proto2ros/proto2ros/utilities.py b/proto2ros/proto2ros/utilities.py deleted file mode 100644 index 15ee2ef..0000000 --- a/proto2ros/proto2ros/utilities.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -"""This module provides various utilities used across the proto2ros package.""" - -import collections -import functools -import itertools -import keyword -from typing import Any, Callable, Iterable, Optional, Tuple, Union - -from rosidl_adapter.parser import BaseType, Type - - -def identity_lru_cache(maxsize: int = 128) -> Callable[[Callable], Callable]: - """A `functools.lru_cache` equivalent that uses objects' id() for result caching.""" - - def _decorator(user_function: Callable) -> Callable: - cache: collections.OrderedDict = collections.OrderedDict() - - @functools.wraps(user_function) - def _wrapper(*args: Any, **kwargs: Any) -> Any: - nonlocal cache - if len(cache) + 1 >= maxsize: - cache.popitem(last=False) - key = (tuple(id(arg) for arg in args), tuple((key, id(value)) for key, value in kwargs.items())) - if key not in cache: - cache[key] = user_function(*args, **kwargs) - return cache[key] - - return _wrapper - - return _decorator - - -def fqn(obj: Any) -> Optional[str]: - """Computes the fully qualified name of a given object, if any.""" - if not hasattr(obj, "__qualname__"): - return None - name = obj.__qualname__ - if not hasattr(obj, "__module__"): - return name - module = obj.__module__ - return f"{module}.{name}" - - -def pairwise(iterable: Iterable[Any]) -> Iterable[Tuple[Any, Any]]: - """Yields an iterable over consecutive pairs.""" - a, b = itertools.tee(iterable) - next(b, None) - return zip(a, b) - - -def rreplace(s: str, old: str, new: str, count: int) -> str: - """Replaces a number of string occurrences in reverse.""" - return s[::-1].replace(old[::-1], new[::-1], count)[::-1] - - -def to_ros_base_type(type_: Union[str, BaseType]) -> str: - """Returns base type name for a given ROS type.""" - return BaseType.__str__(Type(str(type_))) - - -PYTHON_RESERVED_KEYWORD_SET = set(keyword.kwlist) -CPP_RESERVED_KEYWORD_SET = { - "NULL", - "alignas", - "alignof", - "and", - "and_eq", - "asm", - "assert", - "auto", - "bitand", - "bitor", - "bool", - "break", - "case", - "catch", - "char", - "class", - "compl", - "const", - "constexpr", - "const_cast", - "continue", - "decltype", - "default", - "delete", - "do", - "double", - "dynamic_cast", - "else", - "enum", - "explicit", - "export", - "extern", - "false", - "float", - "for", - "friend", - "goto", - "if", - "inline", - "int", - "long", - "mutable", - "namespace", - "new", - "noexcept", - "not", - "not_eq", - "nullptr", - "operator", - "or", - "or_eq", - "private", - "protected", - "public", - "register", - "reinterpret_cast", - "return", - "short", - "signed", - "sizeof", - "static", - "static_assert", - "static_cast", - "struct", - "switch", - "template", - "this", - "thread_local", - "throw", - "true", - "try", - "typedef", - "typeid", - "typename", - "union", - "unsigned", - "using", - "virtual", - "void", - "volatile", - "wchar_t", - "while", - "xor", - "xor_eq", - "char8_t", - "char16_t", - "char32_t", - "concept", - "consteval", - "constinit", - "co_await", - "co_return", - "co_yield", - "requires", -} -RESERVED_KEYWORD_SET = PYTHON_RESERVED_KEYWORD_SET | CPP_RESERVED_KEYWORD_SET - - -def to_protobuf_field_name(name: str) -> str: - """Transform a given name to be a valid Protobuf message field name.""" - if name in RESERVED_KEYWORD_SET: - return name + "_" - return name - - -def to_ros_field_name(name: str) -> str: - """Transform a given name to be a valid ROS message field name.""" - name = name.lower() - if name in RESERVED_KEYWORD_SET: - name = name + "_field" - return name diff --git a/proto2ros/setup.cfg b/proto2ros/setup.cfg deleted file mode 100644 index 464b358..0000000 --- a/proto2ros/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. -[options.entry_points] -console_scripts = - generate = proto2ros.cli.generate:main diff --git a/proto2ros/src/conversions.cpp b/proto2ros/src/conversions.cpp deleted file mode 100644 index e507a85..0000000 --- a/proto2ros/src/conversions.cpp +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) 2024 Boston Dynamics AI Institute LLC. All rights reserved. - -#include "proto2ros/conversions.hpp" - -#include - -#include -#include - -namespace proto2ros::conversions { - -void Convert(const proto2ros::msg::AnyProto& ros_msg, google::protobuf::Any* proto_msg) { - proto_msg->Clear(); - proto_msg->set_type_url(ros_msg.type_url); - auto* value = proto_msg->mutable_value(); - value->reserve(ros_msg.value.size()); - value->assign(ros_msg.value.begin(), ros_msg.value.end()); -} - -void Convert(const google::protobuf::Any& proto_msg, proto2ros::msg::AnyProto* ros_msg) { - ros_msg->type_url = proto_msg.type_url(); - const auto& value = proto_msg.value(); - ros_msg->value.reserve(value.size()); - ros_msg->value.assign(value.begin(), value.end()); -} - -void Convert(const builtin_interfaces::msg::Duration& ros_msg, google::protobuf::Duration* proto_msg) { - proto_msg->set_seconds(ros_msg.sec); - if (ros_msg.nanosec > static_cast(std::numeric_limits::max())) { - throw std::range_error("builtin_interfaces/Duration does not fit in google.protobuf.Duration"); - } - proto_msg->set_nanos(static_cast(ros_msg.nanosec)); -} - -void Convert(const google::protobuf::Duration& proto_msg, builtin_interfaces::msg::Duration* ros_msg) { - if (proto_msg.seconds() > static_cast(std::numeric_limits::max())) { - throw std::range_error("google.protobuf.Durationdoes not fit in builtin_interfaces/Duration"); - } - ros_msg->sec = static_cast(proto_msg.seconds()); - ros_msg->nanosec = proto_msg.nanos(); -} - -void Convert(const builtin_interfaces::msg::Time& ros_msg, google::protobuf::Timestamp* proto_msg) { - proto_msg->set_seconds(ros_msg.sec); - if (ros_msg.nanosec > static_cast(std::numeric_limits::max())) { - throw std::range_error("builtin_interfaces/Time does not fit in google.protobuf.Timestamp"); - } - proto_msg->set_nanos(static_cast(ros_msg.nanosec)); -} - -void Convert(const google::protobuf::Timestamp& proto_msg, builtin_interfaces::msg::Time* ros_msg) { - if (proto_msg.seconds() > static_cast(std::numeric_limits::max())) { - throw std::range_error("google.protobuf.Timestamp does not fit in builtin_interfaces/Time"); - } - ros_msg->sec = static_cast(proto_msg.seconds()); - ros_msg->nanosec = proto_msg.nanos(); -} - -void Convert(const std_msgs::msg::Float64& ros_msg, google::protobuf::DoubleValue* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -void Convert(const google::protobuf::DoubleValue& proto_msg, std_msgs::msg::Float64* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const std_msgs::msg::Float32& ros_msg, google::protobuf::FloatValue* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -void Convert(const google::protobuf::FloatValue& proto_msg, std_msgs::msg::Float32* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const std_msgs::msg::Int64& ros_msg, google::protobuf::Int64Value* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -void Convert(const google::protobuf::Int64Value& proto_msg, std_msgs::msg::Int64* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const std_msgs::msg::Int32& ros_msg, google::protobuf::Int32Value* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -void Convert(const google::protobuf::Int32Value& proto_msg, std_msgs::msg::Int32* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const std_msgs::msg::UInt64& ros_msg, google::protobuf::UInt64Value* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -void Convert(const google::protobuf::UInt64Value& proto_msg, std_msgs::msg::UInt64* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const std_msgs::msg::UInt32& ros_msg, google::protobuf::UInt32Value* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -/// Converts from google.protobuf.UInt32Value Protobuf messages to std_msgs/UInt32 ROS messages. -void Convert(const google::protobuf::UInt32Value& proto_msg, std_msgs::msg::UInt32* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const std_msgs::msg::Bool& ros_msg, google::protobuf::BoolValue* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -void Convert(const google::protobuf::BoolValue& proto_msg, std_msgs::msg::Bool* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const std_msgs::msg::String& ros_msg, google::protobuf::StringValue* proto_msg) { - proto_msg->set_value(ros_msg.data); -} - -void Convert(const google::protobuf::StringValue& proto_msg, std_msgs::msg::String* ros_msg) { - ros_msg->data = proto_msg.value(); -} - -void Convert(const proto2ros::msg::Bytes& ros_msg, google::protobuf::BytesValue* proto_msg) { - auto* value = proto_msg->mutable_value(); - value->reserve(ros_msg.data.size()); - value->assign(ros_msg.data.begin(), ros_msg.data.end()); -} - -void Convert(const google::protobuf::BytesValue& proto_msg, proto2ros::msg::Bytes* ros_msg) { - const auto& value = proto_msg.value(); - ros_msg->data.reserve(value.size()); - ros_msg->data.assign(value.begin(), value.end()); -} - -void Convert(const proto2ros::msg::Value& ros_msg, google::protobuf::Value* proto_msg) { - proto_msg->Clear(); - switch (ros_msg.kind) { - case proto2ros::msg::Value::NUMBER_VALUE_SET: - proto_msg->set_number_value(ros_msg.number_value); - break; - case proto2ros::msg::Value::STRING_VALUE_SET: - proto_msg->set_string_value(ros_msg.string_value); - break; - case proto2ros::msg::Value::BOOL_VALUE_SET: - proto_msg->set_bool_value(ros_msg.bool_value); - break; - case proto2ros::msg::Value::STRUCT_VALUE_SET: { - if (ros_msg.struct_value.type_name != "proto2ros/Struct") { - std::ostringstream message{"expected proto2ros/Struct message for struct_value member"}; - message << ", got " << ros_msg.struct_value.type_name; - throw std::runtime_error(message.str()); - } - rclcpp::SerializedMessage serialized_message; - const auto& value = ros_msg.struct_value.value; - serialized_message.reserve(value.size()); - auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - std::copy(value.begin(), value.end(), rcl_serialized_message.buffer); - rcl_serialized_message.buffer_length = value.size(); - const auto serde = rclcpp::Serialization{}; - auto typed_ros_message = proto2ros::msg::Struct(); - serde.deserialize_message(&serialized_message, &typed_ros_message); - Convert(typed_ros_message, proto_msg->mutable_struct_value()); - } break; - case proto2ros::msg::Value::LIST_VALUE_SET: { - if (ros_msg.list_value.type_name != "proto2ros/List") { - std::ostringstream message{"expected proto2ros/Struct message for list_value member"}; - message << ", got " << ros_msg.list_value.type_name; - throw std::runtime_error(message.str()); - } - rclcpp::SerializedMessage serialized_message; - const auto& value = ros_msg.list_value.value; - serialized_message.reserve(value.size()); - auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - std::copy(value.begin(), value.end(), rcl_serialized_message.buffer); - rcl_serialized_message.buffer_length = value.size(); - const auto serde = rclcpp::Serialization{}; - auto typed_ros_message = proto2ros::msg::List(); - serde.deserialize_message(&serialized_message, &typed_ros_message); - Convert(typed_ros_message, proto_msg->mutable_list_value()); - } break; - case proto2ros::msg::Value::NULL_VALUE_SET: - proto_msg->set_null_value(google::protobuf::NullValue::NULL_VALUE); - break; - default: - break; - } -} - -void Convert(const google::protobuf::Value& proto_msg, proto2ros::msg::Value* ros_msg) { - switch (proto_msg.kind_case()) { - case google::protobuf::Value::kNumberValue: - ros_msg->number_value = proto_msg.number_value(); - ros_msg->kind = proto2ros::msg::Value::NUMBER_VALUE_SET; - break; - case google::protobuf::Value::kStringValue: - ros_msg->string_value = proto_msg.string_value(); - ros_msg->kind = proto2ros::msg::Value::STRING_VALUE_SET; - break; - case google::protobuf::Value::kBoolValue: - ros_msg->bool_value = proto_msg.bool_value(); - ros_msg->kind = proto2ros::msg::Value::BOOL_VALUE_SET; - break; - case google::protobuf::Value::kStructValue: { - auto typed_ros_message = proto2ros::msg::Struct(); - Convert(proto_msg.struct_value(), &typed_ros_message); - const auto serde = rclcpp::Serialization{}; - rclcpp::SerializedMessage serialized_message; - serde.serialize_message(&typed_ros_message, &serialized_message); - const auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - const auto* begin = rcl_serialized_message.buffer; - const auto* end = begin + rcl_serialized_message.buffer_length; - ros_msg->struct_value.value.assign(begin, end); - ros_msg->struct_value.type_name = "proto2ros/Struct"; - ros_msg->kind = proto2ros::msg::Value::STRUCT_VALUE_SET; - } break; - case google::protobuf::Value::kListValue: { - auto typed_ros_message = proto2ros::msg::List(); - Convert(proto_msg.list_value(), &typed_ros_message); - const auto serde = rclcpp::Serialization{}; - rclcpp::SerializedMessage serialized_message; - serde.serialize_message(&typed_ros_message, &serialized_message); - const auto& rcl_serialized_message = serialized_message.get_rcl_serialized_message(); - const auto* begin = rcl_serialized_message.buffer; - const auto* end = begin + rcl_serialized_message.buffer_length; - ros_msg->list_value.value.assign(begin, end); - ros_msg->list_value.type_name = "proto2ros/List"; - ros_msg->kind = proto2ros::msg::Value::LIST_VALUE_SET; - } break; - case google::protobuf::Value::kNullValue: - ros_msg->kind = proto2ros::msg::Value::NULL_VALUE_SET; - break; - default: - ros_msg->kind = proto2ros::msg::Value::NO_VALUE_SET; - break; - } -} - -void Convert(const proto2ros::msg::List& ros_msg, google::protobuf::ListValue* proto_msg) { - proto_msg->Clear(); - auto* values = proto_msg->mutable_values(); - values->Reserve(static_cast(ros_msg.values.size())); - for (const auto& input_item : ros_msg.values) { - auto* output_item = values->Add(); - Convert(input_item, output_item); - } -} - -void Convert(const google::protobuf::ListValue& proto_msg, proto2ros::msg::List* ros_msg) { - ros_msg->values.clear(); - ros_msg->values.reserve(proto_msg.values_size()); - for (const auto& input_item : proto_msg.values()) { - auto& output_item = ros_msg->values.emplace_back(); - Convert(input_item, &output_item); - } -} - -void Convert(const proto2ros::msg::Struct& ros_msg, google::protobuf::Struct* proto_msg) { - proto_msg->Clear(); - auto* output_fields = proto_msg->mutable_fields(); - for (const auto& field : ros_msg.fields) { - Convert(field.value, &(*output_fields)[field.key]); - } -} - -void Convert(const google::protobuf::Struct& proto_msg, proto2ros::msg::Struct* ros_msg) { - ros_msg->fields.clear(); - const auto& input_fields = proto_msg.fields(); - ros_msg->fields.reserve(input_fields.size()); - for (const auto& [key, value] : input_fields) { - auto& field = ros_msg->fields.emplace_back(); - field.key = key; - Convert(value, &field.value); - } -} - -} // namespace proto2ros::conversions diff --git a/proto2ros_tests/CMakeLists.txt b/proto2ros_tests/CMakeLists.txt deleted file mode 100644 index c9fbf9d..0000000 --- a/proto2ros_tests/CMakeLists.txt +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. -cmake_minimum_required(VERSION 3.12) -if(POLICY CMP0148) - cmake_policy(SET CMP0148 OLD) # to accommodate rosidl pipeline -endif() -project(proto2ros_tests) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -find_program(CCACHE_PROGRAM ccache) -if(CCACHE_PROGRAM) - set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") -endif() - -set(DEFAULT_BUILD_TYPE "Release") -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message(STATUS "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.") - set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of the build." FORCE) - # Set the possible values of the build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") -endif() - -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -find_package(ament_cmake REQUIRED) -find_package(builtin_interfaces REQUIRED) -find_package(geometry_msgs REQUIRED) -find_package(sensor_msgs REQUIRED) -find_package(rclcpp REQUIRED) - -find_package(proto2ros REQUIRED) - -find_package(rosidl_default_generators REQUIRED) - -add_subdirectory(proto) - -proto2ros_generate( - ${PROJECT_NAME}_messages_gen - PROTOS proto/test.proto - CONFIG_OVERLAYS config/overlay.yaml - INTERFACES_OUT_VAR ros_messages - PYTHON_OUT_VAR py_sources - CPP_OUT_VAR cpp_sources - INCLUDE_OUT_VAR cpp_include_dir - APPEND_PYTHONPATH "${PROTO_OUT_DIR}" - DEPENDS ${PROJECT_NAME}_proto_gen -) - -rosidl_generate_interfaces( - ${PROJECT_NAME} ${ros_messages} - DEPENDENCIES geometry_msgs sensor_msgs builtin_interfaces proto2ros - SKIP_INSTALL -) -add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}_messages_gen) - -add_library(${PROJECT_NAME}_conversions SHARED ${cpp_sources} src/manual_conversions.cpp) -set_target_properties(${PROJECT_NAME}_conversions PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) -target_compile_options(${PROJECT_NAME}_conversions PUBLIC "$<$:-O0>") -target_compile_definitions(${PROJECT_NAME}_conversions - PUBLIC - "$<$:DEBUG>" - "$<$,$>:NDEBUG>" -) -target_include_directories(${PROJECT_NAME}_conversions PUBLIC - "$" - "$" - "$" -) -rosidl_get_typesupport_target(${PROJECT_NAME}_cpp_msgs ${PROJECT_NAME} "rosidl_typesupport_cpp") -target_link_libraries(${PROJECT_NAME}_conversions - ${${PROJECT_NAME}_cpp_msgs} ${PROJECT_NAME}_proto protobuf::libprotobuf) -ament_target_dependencies(${PROJECT_NAME}_conversions - builtin_interfaces geometry_msgs sensor_msgs proto2ros rclcpp) - -rosidl_generated_python_package_add( - ${PROJECT_NAME}_additional_modules - MODULES ${proto_py_sources} ${py_sources} - PACKAGES ${PROJECT_NAME} - DESTINATION ${PROJECT_NAME} -) - -find_program(CLANG_TIDY_EXECUTABLE NAMES "clang-tidy" REQUIRED) -set(CXX_CLANG_TIDY "${CLANG_TIDY_EXECUTABLE}" "-header-filter=.*proto2ros.*/.*conversions.hpp") -set_target_properties(${PROJECT_NAME}_conversions PROPERTIES CXX_CLANG_TIDY "${CXX_CLANG_TIDY}") - -if(BUILD_TESTING) - find_package(ament_cmake_gtest REQUIRED) - find_package(ament_cmake_pytest REQUIRED) - - get_rosidl_generated_interfaces_test_setup( - LIBRARY_DIRS additional_library_dirs - ENV additional_env - ) - list(APPEND additional_env "PYTHONPATH=${PROTO_OUT_DIR}") - - ament_add_pytest_test( - regression_py_tests test - APPEND_LIBRARY_DIRS ${additional_library_dirs} - APPEND_ENV ${additional_env} - ENV CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR} - PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python - ) - - ament_add_gtest( - regression_cpp_tests test/test_proto2ros.cpp - APPEND_LIBRARY_DIRS ${additional_library_dirs} - ENV CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR} - ) - target_link_libraries(regression_cpp_tests - ${${PROJECT_NAME}_cpp_msgs} ${PROJECT_NAME}_proto - ${PROJECT_NAME}_conversions proto2ros::proto2ros_conversions - ) - set_target_properties(regression_cpp_tests PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) - set_target_properties(regression_cpp_tests PROPERTIES CXX_CLANG_TIDY "${CXX_CLANG_TIDY}") -endif() - -ament_package() diff --git a/proto2ros_tests/LICENSE b/proto2ros_tests/LICENSE deleted file mode 100644 index aa696ca..0000000 --- a/proto2ros_tests/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Boston Dynamics AI Institute - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/proto2ros_tests/config/overlay.yaml b/proto2ros_tests/config/overlay.yaml deleted file mode 100644 index 679d389..0000000 --- a/proto2ros_tests/config/overlay.yaml +++ /dev/null @@ -1,27 +0,0 @@ -any_expansions: - proto2ros_tests.Goal.target: bosdyn.api.SE2Pose - proto2ros_tests.Goal.roi: [bosdyn.api.Polygon, bosdyn.api.Circle] -message_mapping: - bosdyn.api.Vec3: geometry_msgs/Vector3 - bosdyn.api.Quaternion: geometry_msgs/Quaternion - bosdyn.api.SE2Pose: geometry_msgs/Pose - bosdyn.api.Polygon: geometry_msgs/Polygon - bosdyn.api.Circle: geometry_msgs/Vector3 - bosdyn.api.SE3Pose: geometry_msgs/Pose - bosdyn.api.SE3Velocity: geometry_msgs/Twist - bosdyn.api.Wrench: geometry_msgs/Wrench - proto2ros_tests.Temperature: sensor_msgs/Temperature -python_imports: - - bosdyn.api.geometry_pb2 - - geometry_msgs.msg -cpp_headers: - - bosdyn/api/geometry.pb.h - - geometry_msgs/msg/polygon.hpp - - geometry_msgs/msg/pose.hpp - - geometry_msgs/msg/quaternion.hpp - - geometry_msgs/msg/twist.hpp - - geometry_msgs/msg/vector3.hpp - - geometry_msgs/msg/wrench.hpp - - proto2ros_tests/manual_conversions.hpp -inline_python_imports: - - proto2ros_tests.manual_conversions diff --git a/proto2ros_tests/include/proto2ros_tests/manual_conversions.hpp b/proto2ros_tests/include/proto2ros_tests/manual_conversions.hpp deleted file mode 100644 index 4e2025b..0000000 --- a/proto2ros_tests/include/proto2ros_tests/manual_conversions.hpp +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2024 Boston Dynamics AI Institute LLC. All rights reserved. - -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace proto2ros_tests::conversions { - -/// Converts from geometry_msgs/Vector3 ROS messages to bosdyn.api.Vec3 Protobuf messages. -void Convert(const geometry_msgs::msg::Vector3& ros_msg, bosdyn::api::Vec3* proto_msg); - -/// Converts from bosdyn.api.Vec3 Protobuf messages to geometry_msgs/Vector3 ROS messages. -void Convert(const bosdyn::api::Vec3& proto_msg, geometry_msgs::msg::Vector3* ros_msg); - -/// Converts from geometry_msgs/Point ROS messages to bosdyn.api.Vec3 Protobuf messages. -void Convert(const geometry_msgs::msg::Point& ros_msg, bosdyn::api::Vec3* proto_msg); - -/// Converts from bosdyn.api.Vec3 Protobuf messages to geometry_msgs/Point ROS messages. -void Convert(const bosdyn::api::Vec3& proto_msg, geometry_msgs::msg::Point* ros_msg); - -/// Converts from geometry_msgs/Quaternion ROS messages to bosdyn.api.Quaternion Protobuf messages. -void Convert(const geometry_msgs::msg::Quaternion& ros_msg, bosdyn::api::Quaternion* proto_msg); - -/// Converts from bosdyn.api.Quaternion Protobuf messages to geometry_msgs/Quaternion ROS messages. -void Convert(const bosdyn::api::Quaternion& proto_msg, geometry_msgs::msg::Quaternion* ros_msg); - -/// Converts from geometry_msgs/Pose ROS messages to bosdyn.api.SE3Pose Protobuf messages. -void Convert(const geometry_msgs::msg::Pose& ros_msg, bosdyn::api::SE3Pose* proto_msg); - -/// Converts from bosdyn.api.SE3Pose Protobuf messages to geometry_msgs/Pose ROS messages. -void Convert(const bosdyn::api::SE3Pose& proto_msg, geometry_msgs::msg::Pose* ros_msg); - -/// Converts from geometry_msgs/Pose ROS messages to bosdyn.api.SE2Pose Protobuf messages. -void Convert(const geometry_msgs::msg::Pose& ros_msg, bosdyn::api::SE2Pose* proto_msg); - -/// Converts from bosdyn.api.SE2Pose Protobuf messages to geometry_msgs/Pose ROS messages. -void Convert(const bosdyn::api::SE2Pose& proto_msg, geometry_msgs::msg::Pose* ros_msg); - -/// Converts from geometry_msgs/Polygon ROS messages to bosdyn.api.Polygon Protobuf messages. -void Convert(const geometry_msgs::msg::Polygon& ros_msg, bosdyn::api::Polygon* proto_msg); - -/// Converts from bosdyn.api.Polygon Protobuf messages to geometry_msgs/Polygon ROS messages. -void Convert(const bosdyn::api::Polygon& proto_msg, geometry_msgs::msg::Polygon* ros_msg); - -/// Converts from geometry_msgs/Vector3 ROS messages to bosdyn.api.Circle Protobuf messages. -void Convert(const geometry_msgs::msg::Vector3& ros_msg, bosdyn::api::Circle* proto_msg); - -/// Converts from bosdyn.api.Circle Protobuf messages to geometry_msgs/Vector3 ROS messages. -void Convert(const bosdyn::api::Circle& proto_msg, geometry_msgs::msg::Vector3* ros_msg); - -/// Converts from geometry_msgs/Twist ROS messages to bosdyn.api.SE3Velocity Protobuf messages. -void Convert(const geometry_msgs::msg::Twist& ros_msg, bosdyn::api::SE3Velocity* proto_msg); - -/// Converts from bosdyn.api.SE3Velocity Protobuf messages to geometry_msgs/Twist ROS messages. -void Convert(const bosdyn::api::SE3Velocity& proto_msg, geometry_msgs::msg::Twist* ros_msg); - -/// Converts from geometry_msgs/Wrench ROS messages to bosdyn.api.Wrench Protobuf messages. -void Convert(const geometry_msgs::msg::Wrench& ros_msg, bosdyn::api::Wrench* proto_msg); - -/// Converts from bosdyn.api.Wrench Protobuf messages to geometry_msgs/Wrench ROS messages. -void Convert(const bosdyn::api::Wrench& proto_msg, geometry_msgs::msg::Wrench* ros_msg); - -/// Converts from sensor_msgs/Temperature ROS messages to proto2ros_tests.Temperature Protobuf messages. -void Convert(const sensor_msgs::msg::Temperature& ros_msg, Temperature* proto_msg); - -/// Converts from proto2ros_tests.Temperature Protobuf messages to sensor_msgs/Temperature ROS messages. -void Convert(const Temperature& proto_msg, sensor_msgs::msg::Temperature* ros_msg); - -} // namespace proto2ros_tests::conversions diff --git a/proto2ros_tests/package.xml b/proto2ros_tests/package.xml deleted file mode 100644 index cf12c40..0000000 --- a/proto2ros_tests/package.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - proto2ros_tests - 0.1.0 - Protobuf to ROS 2 interoperability interfaces tests - BD AI Institute - MIT - - ament_cmake - rosidl_default_generators - - clang-tidy - - protobuf - protobuf-dev - proto2ros - - rclcpp - builtin_interfaces - geometry_msgs - sensor_msgs - - python3-jinja2 - python3-networkx - python3-protobuf - python3-yaml - - ament_cmake_gtest - ament_cmake_pytest - - rosidl_interface_packages - - - ament_cmake - - diff --git a/proto2ros_tests/proto/CMakeLists.txt b/proto2ros_tests/proto/CMakeLists.txt deleted file mode 100644 index 2b1f899..0000000 --- a/proto2ros_tests/proto/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. -find_package(Protobuf REQUIRED) - -file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bosdyn/api") -file(TOUCH "${CMAKE_CURRENT_BINARY_DIR}/bosdyn/__init__.py") -file(TOUCH "${CMAKE_CURRENT_BINARY_DIR}/bosdyn/api/__init__.py") - -protobuf_generate( - LANGUAGE python - OUT_VAR bosdyn_proto_py_sources - PROTOS bosdyn/api/geometry.proto -) -protobuf_generate( - LANGUAGE python - OUT_VAR proto_py_sources - PROTOS test.proto -) - -protobuf_generate( - LANGUAGE cpp - OUT_VAR bosdyn_proto_cpp_sources - PROTOS bosdyn/api/geometry.proto -) -protobuf_generate( - LANGUAGE cpp - OUT_VAR proto_cpp_sources - PROTOS test.proto -) - -add_library(${PROJECT_NAME}_proto SHARED ${proto_cpp_sources} ${bosdyn_proto_cpp_sources}) -target_include_directories(${PROJECT_NAME}_proto PUBLIC "${CMAKE_CURRENT_BINARY_DIR}") -target_compile_options(${PROJECT_NAME}_proto PUBLIC -Wno-deprecated-declarations) -target_link_libraries(${PROJECT_NAME}_proto protobuf::libprotobuf) - -add_custom_target( - ${PROJECT_NAME}_proto_gen ALL DEPENDS - ${proto_py_sources} ${bosdyn_proto_py_sources} - ${proto_cpp_sources} ${bosdyn_proto_cpp_sources} -) - -set(PROTO_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}" PARENT_SCOPE) diff --git a/proto2ros_tests/proto/bosdyn/api/geometry.proto b/proto2ros_tests/proto/bosdyn/api/geometry.proto deleted file mode 100644 index 1a934e5..0000000 --- a/proto2ros_tests/proto/bosdyn/api/geometry.proto +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved. -// -// Downloading, reproducing, distributing or otherwise using the SDK Software -// is subject to the terms and conditions of the Boston Dynamics Software -// Development Kit License (20191101-BDSDK-SL). - -// Taken verbatim from https://raw.githubusercontent.com/boston-dynamics/spot-sdk/v3.3.0/protos/bosdyn/api/geometry.proto - -syntax = "proto3"; - -package bosdyn.api; - -import "google/protobuf/wrappers.proto"; - -option go_package = "bosdyn/api"; -option java_outer_classname = "GeometryProto"; - -// Two dimensional vector primitive. -message Vec2 { - double x = 1; - double y = 2; -} - -// Three dimensional vector primitive. -message Vec3 { - double x = 1; - double y = 2; - double z = 3; -} - -// Cylindrical coordinates are a generalization of polar coordiates, adding a -// height -// axis. See (http://mathworld.wolfram.com/CylindricalCoordinates.html) for -// more details. -message CylindricalCoordinate { - double r = 1; // Radial coordinate - double theta = 2; // Azimuthal coordinate - double z = 3; // Vertical coordiante -} - -// Quaternion primitive. A quaternion can be used to describe the rotation. -message Quaternion { - double x = 1; - double y = 2; - double z = 3; - double w = 4; -} - -// Plane primitive, described with a point and normal. -message Plane { - Vec3 point = 1; // A point on the plane. - Vec3 normal = 2; // The direction of the planes normal. -} - -// A square oriented in 3D space. -message Quad { - // The center of the quad and the orientation of the normal. - // The normal axis is [0, 0, 1]. - SE3Pose pose = 1; - // The side length of the quad. - double size = 2; -} - -// A ray in 3D space. -message Ray { - // Base of ray. - Vec3 origin = 1; - - // Unit vector defining the direction of the ray. - Vec3 direction = 2; -} - -// Geometric primitive to describe 2D position and rotation. -message SE2Pose { - Vec2 position = 1; // (m) - double angle = 2; // (rad) -} - -// Geometric primitive that describes a 2D velocity through it's linear and angular components. -message SE2Velocity { - Vec2 linear = 1; // (m/s) - double angular = 2; // (rad/s) -} - -// Geometric primitive to couple minimum and maximum SE2Velocities in a single message. -message SE2VelocityLimit { - // If set, limits the maximum velocity. - SE2Velocity max_vel = 1; - // If set, limits the minimum velocity. - SE2Velocity min_vel = 2; -} - -// Geometric primitive to describe 3D position and rotation. -message SE3Pose { - Vec3 position = 1; // (m) - Quaternion rotation = 2; -} - -// Geometric primitive that describes a 3D velocity through it's linear and angular components. -message SE3Velocity { - Vec3 linear = 1; // (m/s) - Vec3 angular = 2; // (rad/s) -} - -// Geometric primitive used to specify forces and torques. -message Wrench { - Vec3 force = 1; // (N) - Vec3 torque = 2; // (Nm) -} - -message FrameTreeSnapshot { - /** - * A frame is a named location in space. \ - * For example, the following frames are defined by the API: \ - * - "body": A frame centered on the robot's body. \ - * - "vision": A non-moving (inertial) frame that is the robot's best - * estimate of a fixed location in the world. It is based on - * both dead reckoning and visual analysis of the world. \ - * - "odom": A non-moving (inertial) frame that is based on the kinematic - * odometry of the robot only. \ - * Additional frames are available for robot joints, sensors, and items - * detected in the world. \ - * - * The FrameTreeSnapshot represents the relationships between the frames that the robot - * knows about at a particular point in time. For example, with the FrameTreeSnapshot, - * an API client can determine where the "body" is relative to the "vision". \ - * - * To reduce data bandwidth, the FrameTreeSnapshot will typically contain - * a small subset of all known frames. By default, all services MUST - * include "vision", "body", and "odom" frames in the FrameTreeSnapshot, but - * additional frames can also be included. For example, an Image service - * would likely include the frame located at the base of the camera lens - * where the picture was taken. \ - * - * Frame relationships are expressed as edges between "parent" frames and - * "child" frames, with an SE3Pose indicating the pose of the "child" frame - * expressed in the "parent" frame. These edges are included in the edge_map - * field. For example, if frame "hand" is 1m in front of the frame "shoulder", - * then the FrameTreeSnapshot might contain: \ - * edge_map { \ - * key: "hand" \ - * value: { \ - * parent_frame_name: "shoulder" \ - * parent_tform_child: { \ - * position: { \ - * x: 1.0 \ - * y: 0.0 \ - * z: 0.0 \ - * } \ - * } \ - * } \ - * } \ - * - * Frame relationships can be inverted. So, to find where the "shoulder" - * is in relationship the "hand", the parent_tform_child pose in the edge - * above can be inverted: \ - * hand_tform_shoulder = shoulder_tform_hand.inverse() \ - * Frame relationships can also be concatenated. If there is an additional - * edge specifying the pose of the "shoulder" relative to the "body", then - * to find where the "hand" is relative to the "body" do: \ - * body_tform_hand = body_tform_shoulder * shoulder_tform_hand \ - * - * The two properties above reduce data size. Instead of having to send N^2 - * edge_map entries to represent all relationships between N frames, - * only N edge_map entries need to be sent. Clients will need to determine - * the chain of edges to follow to get from one frame to another frae, - * and then do inversion and concatentation to generate the appropriate pose. \ - * - * Note that all FrameTreeSnapshots are expected to be a single rooted tree. - * The syntax for FrameTreeSnapshot could also support graphs with - * cycles, or forests of trees - but clients should treat those as invalid - * representations. \ - */ - - // ParentEdge represents the relationship from a child frame to a parent frame. - message ParentEdge { - // The name of the parent frame. If a frame has no parent (parent_frame_name is empty), - // it is the root of the tree. - string parent_frame_name = 1; - - // Transform representing the pose of the child frame in the parent's frame. - SE3Pose parent_tform_child = 2; - - } - - // child_to_parent_edge_map maps the child frame name to the ParentEdge. - // In aggregate, this forms the tree structure. - map child_to_parent_edge_map = 1; -} - - -// Geometric primitive describing a two-dimensional box. -message Box2 { - Vec2 size = 1; -} - -// Geometric primitive to describe a 2D box in a specific frame. -message Box2WithFrame { - // The box is specified with width (y) and length (x), and the full box is - // fixed at an origin, where it's sides are along the coordinate frame's - // axes. - Box2 box = 1; - // The pose of the axis-aligned box is in 'frame_name'. - string frame_name = 2; - // The transformation of the axis-aligned box into the desired frame - // (specified above). - SE3Pose frame_name_tform_box = 3; -} - -// Geometric primitive describing a three-dimensional box. -message Box3 { - Vec3 size = 1; -} - -// Geometric primitive to describe a 3D box in a specific frame. -message Box3WithFrame { - // The box width (y), length (x), and height (z) are interpreted in, and the - // full box is fixed at an origin, where it's sides are along the coordinate - // frame's axes. - Box3 box = 1; - // The pose of the axis-aligned box is in 'frame_name'. - string frame_name = 2; - // The transformation of the axis-aligned box into the desired frame - // (specified above). - SE3Pose frame_name_tform_box = 3; -} - -// Represents a row-major order matrix of doubles. -message Matrix { - int32 rows = 1; - int32 cols = 2; - repeated double values = 3; -} - -// Represents a row-major order matrix of floats. -message Matrixf { - int32 rows = 1; - int32 cols = 2; - repeated float values = 3; -} - -// Represents a row-major order matrix of int64. -message MatrixInt64 { - int32 rows = 1; - int32 cols = 2; - repeated int64 values = 3; -} - -// Represents a row-major order matrix of int32. -message MatrixInt32 { - int32 rows = 1; - int32 cols = 2; - repeated int32 values = 3; -} - -// Represents a vector of doubles -message Vector { - repeated double values = 1; -} - -// Represents the translation/rotation covariance of an SE3 Pose. -// The 6x6 matrix can be viewed as the covariance among 6 variables: \ -// rx ry rz x y z \ -// rx rxrx rxry rxrz rxx rxy rxz \ -// ry ryrx ryry ryrz ryx ryy ryz \ -// rz rzrx rzry rzrz rzx rzy rzz \ -// x xrx xry xrz xx xy xz \ -// y yrx yry yrz yx yy yz \ -// z zrx zry zrz zx zy zz \ -// where x, y, z are translations in meters, and rx, ry, rz are rotations around -// the x, y and z axes in radians. \ -// The matrix is symmetric, so, for example, xy = yx. \ -message SE3Covariance { -// Row-major order representation of the covariance matrix. -Matrix matrix = 1; -// Variance of the yaw component of the SE3 Pose. -// Warning: DEPRECATED as of 2.1. This should equal cov_rzrz, inside `matrix`. Will be removed -// in 4.0. -double yaw_variance = 2 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_xx = 3 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_xy = 4 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_xz = 5 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_yx = 6 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_yy = 7 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_yz = 8 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_zx = 9 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_zy = 10 [deprecated = true]; -// Warning: DEPRECATED as of 2.1. Use 'matrix.' Will be removed in 4.0. -double cov_zz = 11 [deprecated = true]; -} - -// Multi-part, 1D line segments defined by a series of points. -message PolyLine { - repeated bosdyn.api.Vec2 points = 1; -} - -// Polygon in the XY plane. -// May be concave, but should not self-intersect. Vertices can be specified in either -// clockwise or counterclockwise orders. -message Polygon { - repeated Vec2 vertexes = 1; -} - -// Represents a region in the XY plane that consists of a single polygon -// from which polygons representing exclusion areas may be subtracted. -// -// A point is considered to be inside the region if it is inside the inclusion -// polygon and not inside any of the exclusion polygons. -// -// Note that while this can be used to represent a polygon with holes, that -// exclusions are not necessarily holes: An exclusion polygon may not be -// completely inside the inclusion polygon. -message PolygonWithExclusions { - Polygon inclusion = 5; - repeated Polygon exclusions = 6; -} - - -// Represents a circular 2D area. -message Circle { - bosdyn.api.Vec2 center_pt = 1; - double radius = 2; // Dimensions in m from center_pt. -} - -// Represents an area in the XY plane. -message Area { - oneof geometry { - Polygon polygon = 1; - Circle circle = 2; - } -} - -// Represents a volume of space in an unspecified frame. -message Volume { - oneof geometry { - Vec3 box = 1; // Dimensions in m, centered on frame origin. - } -} - -// Represents bounds on a value, such that lower < value < upper. -// If you do not want to specify one side of the bound, set it to -// an appropriately large (or small) number. -message Bounds { - double lower = 1; - double upper = 2; -} - -// A 2D vector of doubles that uses wrapped values so we can tell which elements are set. -message Vec2Value { - google.protobuf.DoubleValue x = 1; - google.protobuf.DoubleValue y = 2; -} - -// A 3D vector of doubles that uses wrapped values so we can tell which elements are set. -message Vec3Value { - google.protobuf.DoubleValue x = 1; - google.protobuf.DoubleValue y = 2; - google.protobuf.DoubleValue z = 3; -} diff --git a/proto2ros_tests/proto/test.proto b/proto2ros_tests/proto/test.proto deleted file mode 100644 index 6e17ddb..0000000 --- a/proto2ros_tests/proto/test.proto +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -syntax = "proto3"; - -package proto2ros_tests; - -option java_outer_classname = "Proto2ROSTests"; - -import "google/protobuf/any.proto"; -import "google/protobuf/type.proto"; - -import "bosdyn/api/geometry.proto"; - -// Body relative directions for motion. -enum Direction { - LEFT = 0; - RIGHT = 1; - FORWARD = 2; - BACKWARD = 3; -} - -// Temperature measurement. -message Temperature { - // Measurement temperature scale. - enum Scale { - CELSIUS = 0; - KELVIN = 1; - FAHRENHEIT = 2; - } - float value = 1; - Scale scale = 2; -} - -// A control request for an HVAC system. -message HVACControlRequest { - double air_flow_rate = 1; - Temperature temperature_setpoint = 2; -} - -// Binary recursive fragment. -message Fragment { - // fragment payload. - bytes payload = 1; - // nested fragments. - repeated Fragment nested = 2; -} - -// Heterogeneous pair. -message Pair { - // First value. - Value first = 1; - // Second value. - Value second = 2; -} - -// Heterogeneous list. -message List { - // Listed values. - repeated Value values = 1; -} - -// Heterogeneous dict. -message Dict { - map items = 1; -} - -// Heterogeneous value. -message Value { - oneof data { - // Numeric value. - float number = 1; - // Text value. - string text = 2; - // Pair value. - Pair pair = 3; - // List value. - List list = 4; - // Dict value. - Dict dict = 5; - } -} - -// Discrete motion request. -message MotionRequest { - // Valid directions for motion. - enum Direction { - Left = 0; - Right = 1; - Forward = 2; - Backward = 3; - } - // Direction of motion. - Direction direction = 1; - // motion speed. - float speed = 2; -} - -// Empty HTTP message. -/* - * For namespacing purposes only. - */ -message HTTP { - // Supported HTTP methods. - enum Method { - GET = 0; - HEAD = 1; - POST = 2; - PUT = 3; - } - // HTTP request message. - message Request { - // HTTP request method. - Method method = 1; - // HTTP resource URI. - string uri = 2; - // HTTP request body. - bytes body = 3; - } - // HTTP status codes. - enum Status { - UNDEFINED = 0; - OK = 200; - NOT_FOUND = 404; - INTERNAL_SERVER_ERROR = 500; - } - // HTTP response message. - message Response { - // HTTP response status. - Status status = 1; - // HTTP response reason. - string reason = 2; - // HTTP response body. - bytes body = 3; - } -} - -// Any single robot command. -message AnyCommand { - // Walk command. - message Walk { - // Walking distance. - float distance = 1; - // walking speed. - float speed = 2; - } - // Jump command. - message Jump { - // Jump height. - float height = 1; - } - // Sit command. - message Sit {} - - oneof commands { - Walk walk = 1; - Jump jump = 2; - Sit sit = 3; - } -} - -// Generic diagnostic message. -message Diagnostic { - enum Severity { - // Informational diagnostic severity. - INFO = 0; - WARN = 1; - FATAL = 2; - } - // Diagnostic severity. - Severity severity = 1; - // Diagnostic reason. - string reason = 2; - // Diagnostic attributes. - map attributes = 3; -} - -// Remote RPA request. -message RemoteExecutionRequest { - // Execution prompt. - string prompt = 1; - // Timeout for request, in nanoseconds - int64 timeout = 2 [deprecated = true]; - // Timeout for request, in seconds. - float timeout_sec = 3; -} - -// Generic error message. -message Error { - // Integer error code. - int32 code = 1; - // error reason. - string reason = 2; -} - -// Remote RPA result. -message RemoteExecutionResult { - // Remote execution error. - message Error { - int32 code = 1; - // May contain backslashes (\). - string reason = 2; - repeated string traceback = 3; - } - bool ok = 1; - Error error = 2; -} - - -// 3D displacement -message Displacement { - bosdyn.api.Vec3 translation = 1; - // Rotation field (TBD) - reserved "rotation"; -} - -// Camera sensor information. -message CameraInfo { - message DistortionModel { - enum Type { - // Uses 5 coefficients. - PLUMB_BOB = 0; - // Uses 4 coefficients. - EQUIDISTANT = 1; - } - Type type = 1; - repeated double coefficients = 2; - } - - uint32 height = 1; - uint32 width = 2; - - // Intrinsic matrix. - Matrix K = 3; - // Rectification matrix. - Matrix R = 4; - // Projection matrix. - Matrix P = 5; - - DistortionModel distortion_model = 6; -} - -message Matrix { - uint32 rows = 1; - uint32 cols = 2; - // Row-major matrix data. - repeated double data = 3; -} - -// Generic map message. -message Map { - message Fragment { - int32 width = 1; - int32 height = 2; - // Fragment grid as a column-major matrix. - bytes grid = 3; - } - map submaps = 1; -} - -// Protobuf type query request message. -message RTTIQueryRequest { - google.protobuf.Any msg = 1; -} - -// Protobuf type query result message. -message RTTIQueryResult { - google.protobuf.Type type = 1; -} - -// Generalized goal. -message Goal { - google.protobuf.Any target = 1; - google.protobuf.Any roi = 2; -} diff --git a/proto2ros_tests/proto2ros_tests/__init__.py b/proto2ros_tests/proto2ros_tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/proto2ros_tests/proto2ros_tests/manual_conversions.py b/proto2ros_tests/proto2ros_tests/manual_conversions.py deleted file mode 100644 index 09dd61c..0000000 --- a/proto2ros_tests/proto2ros_tests/manual_conversions.py +++ /dev/null @@ -1,187 +0,0 @@ -import math - -import bosdyn.api.geometry_pb2 -import geometry_msgs.msg -import sensor_msgs.msg -import test_pb2 - -from proto2ros.conversions import convert - - -@convert.register(geometry_msgs.msg.Vector3, bosdyn.api.geometry_pb2.Vec3) -def convert_geometry_msgs_vector3_message_to_bosdyn_api_vec3_proto( - ros_msg: geometry_msgs.msg.Vector3, - proto_msg: bosdyn.api.geometry_pb2.Vec3, -) -> None: - proto_msg.x = ros_msg.x - proto_msg.y = ros_msg.y - proto_msg.z = ros_msg.z - - -@convert.register(bosdyn.api.geometry_pb2.Vec3, geometry_msgs.msg.Vector3) -def convert_bosdyn_api_vec3_proto_to_geometry_msgs_vector3_message( - proto_msg: bosdyn.api.geometry_pb2.Vec3, - ros_msg: geometry_msgs.msg.Vector3, -) -> None: - ros_msg.x = proto_msg.x - ros_msg.y = proto_msg.y - ros_msg.z = proto_msg.z - - -@convert.register(geometry_msgs.msg.Quaternion, bosdyn.api.geometry_pb2.Quaternion) -def convert_geometry_msgs_quaternion_message_to_bosdyn_api_quaternion_proto( - ros_msg: geometry_msgs.msg.Quaternion, - proto_msg: bosdyn.api.geometry_pb2.Quaternion, -) -> None: - proto_msg.w = ros_msg.w - proto_msg.x = ros_msg.x - proto_msg.y = ros_msg.y - proto_msg.z = ros_msg.z - - -@convert.register(bosdyn.api.geometry_pb2.Quaternion, geometry_msgs.msg.Quaternion) -def convert_bosdyn_api_quaternion_proto_to_geometry_msgs_quaternion_message( - proto_msg: bosdyn.api.geometry_pb2.Quaternion, - ros_msg: geometry_msgs.msg.Quaternion, -) -> None: - ros_msg.w = proto_msg.w - ros_msg.x = proto_msg.x - ros_msg.y = proto_msg.y - ros_msg.z = proto_msg.z - - -@convert.register(geometry_msgs.msg.Pose, bosdyn.api.geometry_pb2.SE3Pose) -def convert_geometry_msgs_pose_message_to_bosdyn_api_se3_pose_proto( - ros_msg: geometry_msgs.msg.Pose, - proto_msg: bosdyn.api.geometry_pb2.SE3Pose, -) -> None: - convert_geometry_msgs_vector3_message_to_bosdyn_api_vec3_proto(ros_msg.position, proto_msg.position) - convert_geometry_msgs_quaternion_message_to_bosdyn_api_quaternion_proto(ros_msg.orientation, proto_msg.rotation) - - -@convert.register(bosdyn.api.geometry_pb2.SE3Pose, geometry_msgs.msg.Pose) -def convert_bosdyn_api_se3_pose_proto_to_geometry_msgs_pose_message( - proto_msg: bosdyn.api.geometry_pb2.SE3Pose, - ros_msg: geometry_msgs.msg.Pose, -) -> None: - convert_bosdyn_api_vec3_proto_to_geometry_msgs_vector3_message(proto_msg.position, ros_msg.position) - convert_bosdyn_api_quaternion_proto_to_geometry_msgs_quaternion_message(proto_msg.rotation, ros_msg.orientation) - - -@convert.register(geometry_msgs.msg.Pose, bosdyn.api.geometry_pb2.SE2Pose) -def convert_geometry_msgs_pose_message_to_bosdyn_api_se2_pose_proto( - ros_msg: geometry_msgs.msg.Pose, - proto_msg: bosdyn.api.geometry_pb2.SE2Pose, -) -> None: - proto_msg.position.x = ros_msg.position.x - proto_msg.position.y = ros_msg.position.y - proto_msg.angle = 2.0 * math.acos(ros_msg.orientation.w) - - -@convert.register(bosdyn.api.geometry_pb2.SE2Pose, geometry_msgs.msg.Pose) -def convert_bosdyn_api_se2_pose_proto_to_geometry_msgs_pose_message( - proto_msg: bosdyn.api.geometry_pb2.SE2Pose, - ros_msg: geometry_msgs.msg.Pose, -) -> None: - ros_msg.position.x = proto_msg.position.x - ros_msg.position.y = proto_msg.position.y - ros_msg.orientation.w = math.cos(proto_msg.angle / 2.0) - ros_msg.orientation.z = math.sin(proto_msg.angle / 2.0) - - -@convert.register(geometry_msgs.msg.Polygon, bosdyn.api.geometry_pb2.Polygon) -def convert_geometry_msgs_polygon_message_to_bosdyn_api_polygon_proto( - ros_msg: geometry_msgs.msg.Polygon, - proto_msg: bosdyn.api.geometry_pb2.Polygon, -) -> None: - proto_msg.Clear() - for point in ros_msg.points: - vertex = proto_msg.vertexes.add() - vertex.x = point.x - vertex.y = point.y - - -@convert.register(bosdyn.api.geometry_pb2.Polygon, geometry_msgs.msg.Polygon) -def convert_bosdyn_api_polygon_proto_to_geometry_msgs_polygon_message( - proto_msg: bosdyn.api.geometry_pb2.Polygon, - ros_msg: geometry_msgs.msg.Polygon, -) -> None: - ros_msg.points = [geometry_msgs.msg.Point32(x=vertex.x, y=vertex.y, z=0.0) for vertex in proto_msg.vertexes] - - -@convert.register(geometry_msgs.msg.Vector3, bosdyn.api.geometry_pb2.Circle) -def convert_geometry_msgs_vector3_message_to_bosdyn_api_circle_proto( - ros_msg: geometry_msgs.msg.Vector3, - proto_msg: bosdyn.api.geometry_pb2.Circle, -) -> None: - proto_msg.center_pt.x = ros_msg.x - proto_msg.center_pt.y = ros_msg.y - proto_msg.radius = ros_msg.z - - -@convert.register(bosdyn.api.geometry_pb2.Circle, geometry_msgs.msg.Vector3) -def convert_bosdyn_api_circle_proto_to_geometry_msgs_vector3_message( - proto_msg: bosdyn.api.geometry_pb2.Circle, - ros_msg: geometry_msgs.msg.Vector3, -) -> None: - ros_msg.x = proto_msg.center_pt.x - ros_msg.y = proto_msg.center_pt.y - ros_msg.z = proto_msg.radius - - -@convert.register(geometry_msgs.msg.Twist, bosdyn.api.geometry_pb2.SE3Velocity) -def convert_geometry_msgs_twist_message_to_bosdyn_api_se3_velocity_proto( - ros_msg: geometry_msgs.msg.Twist, - proto_msg: bosdyn.api.geometry_pb2.SE3Velocity, -) -> None: - convert_geometry_msgs_vector3_message_to_bosdyn_api_vec3_proto(ros_msg.linear, proto_msg.linear) - convert_geometry_msgs_vector3_message_to_bosdyn_api_vec3_proto(ros_msg.angular, proto_msg.angular) - - -@convert.register(bosdyn.api.geometry_pb2.SE3Velocity, geometry_msgs.msg.Twist) -def convert_bosdyn_api_se3_velocity_proto_to_geometry_msgs_twist_message( - proto_msg: bosdyn.api.geometry_pb2.SE3Velocity, - ros_msg: geometry_msgs.msg.Twist, -) -> None: - convert_bosdyn_api_vec3_proto_to_geometry_msgs_vector3_message(proto_msg.linear, ros_msg.linear) - convert_bosdyn_api_vec3_proto_to_geometry_msgs_vector3_message(proto_msg.angular, ros_msg.angular) - - -@convert.register(geometry_msgs.msg.Wrench, bosdyn.api.geometry_pb2.Wrench) -def convert_geometry_msgs_wrench_message_to_bosdyn_api_wrench_proto( - ros_msg: geometry_msgs.msg.Wrench, - proto_msg: bosdyn.api.geometry_pb2.Wrench, -) -> None: - convert_geometry_msgs_vector3_message_to_bosdyn_api_vec3_proto(ros_msg.force, proto_msg.force) - convert_geometry_msgs_vector3_message_to_bosdyn_api_vec3_proto(ros_msg.torque, proto_msg.torque) - - -@convert.register(bosdyn.api.geometry_pb2.Wrench, geometry_msgs.msg.Wrench) -def convert_bosdyn_api_wrench_proto_to_geometry_msgs_wrench_message( - proto_msg: bosdyn.api.geometry_pb2.Wrench, - ros_msg: geometry_msgs.msg.Wrench, -) -> None: - convert_bosdyn_api_vec3_proto_to_geometry_msgs_vector3_message(proto_msg.force, ros_msg.force) - convert_bosdyn_api_vec3_proto_to_geometry_msgs_vector3_message(proto_msg.torque, ros_msg.torque) - - -@convert.register(sensor_msgs.msg.Temperature, test_pb2.Temperature) -def convert_sensor_msgs_temperature_message_to_proto2ros_tests_temperature_proto( - ros_msg: sensor_msgs.msg.Temperature, - proto_msg: test_pb2.Temperature, -) -> None: - proto_msg.scale = test_pb2.Temperature.Scale.CELSIUS - proto_msg.value = ros_msg.temperature - - -@convert.register(test_pb2.Temperature, sensor_msgs.msg.Temperature) -def convert_proto2ros_tests_temperature_proto_to_sensor_msgs_temperature_message( - proto_msg: test_pb2.Temperature, - ros_msg: sensor_msgs.msg.Temperature, -) -> None: - if proto_msg.scale == test_pb2.Temperature.Scale.KELVIN: - ros_msg.temperature = proto_msg.value + 273 - elif proto_msg.scale == test_pb2.Temperature.Scale.FAHRENHEIT: - ros_msg.temperature = (proto_msg.value - 32) * 5 / 9 - else: # proto_msg.scale == test_pb2.Temperature.Scale.CELSIUS - ros_msg.temperature = proto_msg.value diff --git a/proto2ros_tests/src/manual_conversions.cpp b/proto2ros_tests/src/manual_conversions.cpp deleted file mode 100644 index 09d9f48..0000000 --- a/proto2ros_tests/src/manual_conversions.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2024 Boston Dynamics AI Institute LLC. All rights reserved. - -#include "proto2ros_tests/manual_conversions.hpp" - -#include - -namespace proto2ros_tests::conversions { - -void Convert(const geometry_msgs::msg::Vector3& ros_msg, bosdyn::api::Vec3* proto_msg) { - proto_msg->set_x(ros_msg.x); - proto_msg->set_y(ros_msg.y); - proto_msg->set_z(ros_msg.z); -} - -void Convert(const bosdyn::api::Vec3& proto_msg, geometry_msgs::msg::Vector3* ros_msg) { - ros_msg->x = proto_msg.x(); - ros_msg->y = proto_msg.y(); - ros_msg->z = proto_msg.z(); -} - -void Convert(const geometry_msgs::msg::Point& ros_msg, bosdyn::api::Vec3* proto_msg) { - proto_msg->set_x(ros_msg.x); - proto_msg->set_y(ros_msg.y); - proto_msg->set_z(ros_msg.z); -} - -void Convert(const bosdyn::api::Vec3& proto_msg, geometry_msgs::msg::Point* ros_msg) { - ros_msg->x = proto_msg.x(); - ros_msg->y = proto_msg.y(); - ros_msg->z = proto_msg.z(); -} - -void Convert(const geometry_msgs::msg::Quaternion& ros_msg, bosdyn::api::Quaternion* proto_msg) { - proto_msg->set_x(ros_msg.x); - proto_msg->set_y(ros_msg.y); - proto_msg->set_z(ros_msg.z); - proto_msg->set_w(ros_msg.w); -} - -void Convert(const bosdyn::api::Quaternion& proto_msg, geometry_msgs::msg::Quaternion* ros_msg) { - ros_msg->x = proto_msg.x(); - ros_msg->y = proto_msg.y(); - ros_msg->z = proto_msg.z(); - ros_msg->w = proto_msg.w(); -} - -void Convert(const geometry_msgs::msg::Pose& ros_msg, bosdyn::api::SE3Pose* proto_msg) { - Convert(ros_msg.position, proto_msg->mutable_position()); - Convert(ros_msg.orientation, proto_msg->mutable_rotation()); -} - -void Convert(const bosdyn::api::SE3Pose& proto_msg, geometry_msgs::msg::Pose* ros_msg) { - Convert(proto_msg.position(), &ros_msg->position); - Convert(proto_msg.rotation(), &ros_msg->orientation); -} - -void Convert(const geometry_msgs::msg::Pose& ros_msg, bosdyn::api::SE2Pose* proto_msg) { - // NOLINTBEGIN(readability-magic-numbers) - proto_msg->Clear(); - proto_msg->mutable_position()->set_x(ros_msg.position.x); - proto_msg->mutable_position()->set_y(ros_msg.position.y); - proto_msg->set_angle(2.0 * std::acos(ros_msg.orientation.w)); - const double angle = 2.0 * std::asin(ros_msg.orientation.z); - if (std::abs(proto_msg->angle() - angle) > std::numeric_limits::epsilon()) { - throw std::domain_error("geometry_msgs/Pose does not represent a bosdyn.api.SE2Pose"); - } - // NOLINTEND(readability-magic-numbers) -} - -void Convert(const bosdyn::api::SE2Pose& proto_msg, geometry_msgs::msg::Pose* ros_msg) { - // NOLINTBEGIN(readability-magic-numbers) - ros_msg->position.x = proto_msg.position().x(); - ros_msg->position.y = proto_msg.position().y(); - ros_msg->position.z = 0.0; - ros_msg->orientation.w = std::cos(proto_msg.angle() / 2.0); - ros_msg->orientation.z = std::sin(proto_msg.angle() / 2.0); - ros_msg->orientation.y = 0.0; - ros_msg->orientation.x = 0.0; - // NOLINTEND(readability-magic-numbers) -} - -void Convert(const geometry_msgs::msg::Polygon& ros_msg, bosdyn::api::Polygon* proto_msg) { - proto_msg->Clear(); - auto* vertices = proto_msg->mutable_vertexes(); - vertices->Reserve(static_cast(ros_msg.points.size())); - for (const auto& point : ros_msg.points) { - auto* vertex = vertices->Add(); - vertex->set_x(point.x); - vertex->set_y(point.y); - } -} - -void Convert(const bosdyn::api::Polygon& proto_msg, geometry_msgs::msg::Polygon* ros_msg) { - ros_msg->points.clear(); - ros_msg->points.reserve(proto_msg.vertexes_size()); - for (const auto& vertex : proto_msg.vertexes()) { - auto& point = ros_msg->points.emplace_back(); - point.x = static_cast(vertex.x()); - point.y = static_cast(vertex.y()); - } -} - -void Convert(const geometry_msgs::msg::Vector3& ros_msg, bosdyn::api::Circle* proto_msg) { - auto* center = proto_msg->mutable_center_pt(); - center->set_x(ros_msg.x); - center->set_y(ros_msg.y); - proto_msg->set_radius(ros_msg.z); -} - -void Convert(const bosdyn::api::Circle& proto_msg, geometry_msgs::msg::Vector3* ros_msg) { - const auto& center = proto_msg.center_pt(); - ros_msg->x = center.x(); - ros_msg->y = center.y(); - ros_msg->z = proto_msg.radius(); -} - -void Convert(const geometry_msgs::msg::Twist& ros_msg, bosdyn::api::SE3Velocity* proto_msg) { - Convert(ros_msg.linear, proto_msg->mutable_linear()); - Convert(ros_msg.angular, proto_msg->mutable_angular()); -} - -void Convert(const bosdyn::api::SE3Velocity& proto_msg, geometry_msgs::msg::Twist* ros_msg) { - Convert(proto_msg.linear(), &ros_msg->linear); - Convert(proto_msg.angular(), &ros_msg->angular); -} - -void Convert(const geometry_msgs::msg::Wrench& ros_msg, bosdyn::api::Wrench* proto_msg) { - Convert(ros_msg.force, proto_msg->mutable_force()); - Convert(ros_msg.torque, proto_msg->mutable_torque()); -} - -void Convert(const bosdyn::api::Wrench& proto_msg, geometry_msgs::msg::Wrench* ros_msg) { - Convert(proto_msg.force(), &ros_msg->force); - Convert(proto_msg.torque(), &ros_msg->torque); -} - -void Convert(const sensor_msgs::msg::Temperature& ros_msg, Temperature* proto_msg) { - proto_msg->set_scale(Temperature::CELSIUS); - proto_msg->set_value(static_cast(ros_msg.temperature)); -} - -void Convert(const Temperature& proto_msg, sensor_msgs::msg::Temperature* ros_msg) { - // NOLINTBEGIN(readability-magic-numbers) - switch (proto_msg.scale()) { - case Temperature::KELVIN: - ros_msg->temperature = proto_msg.value() + 273.0; - break; - case Temperature::FAHRENHEIT: - ros_msg->temperature = (proto_msg.value() - 32.0) * 5.0 / 9.0; - break; - case Temperature::CELSIUS: - default: - ros_msg->temperature = proto_msg.value(); - break; - } - // NOLINTEND(readability-magic-numbers) -} - -} // namespace proto2ros_tests::conversions diff --git a/proto2ros_tests/test/generated/AnyCommand.msg b/proto2ros_tests/test/generated/AnyCommand.msg deleted file mode 100644 index 27ab3ff..0000000 --- a/proto2ros_tests/test/generated/AnyCommand.msg +++ /dev/null @@ -1,3 +0,0 @@ -# Any single robot command. - -proto2ros_tests/AnyCommandOneOfCommands commands diff --git a/proto2ros_tests/test/generated/AnyCommandJump.msg b/proto2ros_tests/test/generated/AnyCommandJump.msg deleted file mode 100644 index 0823e68..0000000 --- a/proto2ros_tests/test/generated/AnyCommandJump.msg +++ /dev/null @@ -1,4 +0,0 @@ -# Jump command. - -# Jump height. -float32 height diff --git a/proto2ros_tests/test/generated/AnyCommandOneOfCommands.msg b/proto2ros_tests/test/generated/AnyCommandOneOfCommands.msg deleted file mode 100644 index 3a51122..0000000 --- a/proto2ros_tests/test/generated/AnyCommandOneOfCommands.msg +++ /dev/null @@ -1,11 +0,0 @@ - -int8 COMMANDS_NOT_SET=0 -int8 COMMANDS_WALK_SET=1 -int8 COMMANDS_JUMP_SET=2 -int8 COMMANDS_SIT_SET=3 - -proto2ros_tests/AnyCommandWalk walk -proto2ros_tests/AnyCommandJump jump -proto2ros_tests/AnyCommandSit sit -int8 commands_choice # deprecated -int8 which diff --git a/proto2ros_tests/test/generated/AnyCommandSit.msg b/proto2ros_tests/test/generated/AnyCommandSit.msg deleted file mode 100644 index fcb5a5a..0000000 --- a/proto2ros_tests/test/generated/AnyCommandSit.msg +++ /dev/null @@ -1 +0,0 @@ -# Sit command. diff --git a/proto2ros_tests/test/generated/AnyCommandWalk.msg b/proto2ros_tests/test/generated/AnyCommandWalk.msg deleted file mode 100644 index 64c7448..0000000 --- a/proto2ros_tests/test/generated/AnyCommandWalk.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Walk command. - -# Walking distance. -float32 distance -# walking speed. -float32 speed diff --git a/proto2ros_tests/test/generated/CameraInfo.msg b/proto2ros_tests/test/generated/CameraInfo.msg deleted file mode 100644 index 855b592..0000000 --- a/proto2ros_tests/test/generated/CameraInfo.msg +++ /dev/null @@ -1,17 +0,0 @@ -# Camera sensor information. - -uint8 K_FIELD_SET=4 -uint8 R_FIELD_SET=8 -uint8 P_FIELD_SET=16 -uint8 DISTORTION_MODEL_FIELD_SET=32 - -uint32 height -uint32 width -# Intrinsic matrix. -proto2ros_tests/Matrix k -# Rectification matrix. -proto2ros_tests/Matrix r -# Projection matrix. -proto2ros_tests/Matrix p -proto2ros_tests/CameraInfoDistortionModel distortion_model -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/CameraInfoDistortionModel.msg b/proto2ros_tests/test/generated/CameraInfoDistortionModel.msg deleted file mode 100644 index c42c3e0..0000000 --- a/proto2ros_tests/test/generated/CameraInfoDistortionModel.msg +++ /dev/null @@ -1,3 +0,0 @@ - -proto2ros_tests/CameraInfoDistortionModelType type -float64[] coefficients diff --git a/proto2ros_tests/test/generated/CameraInfoDistortionModelType.msg b/proto2ros_tests/test/generated/CameraInfoDistortionModelType.msg deleted file mode 100644 index 0cd9cf0..0000000 --- a/proto2ros_tests/test/generated/CameraInfoDistortionModelType.msg +++ /dev/null @@ -1,7 +0,0 @@ - -# Uses 5 coefficients. -int32 PLUMB_BOB=0 -# Uses 4 coefficients. -int32 EQUIDISTANT=1 - -int32 value diff --git a/proto2ros_tests/test/generated/Diagnostic.msg b/proto2ros_tests/test/generated/Diagnostic.msg deleted file mode 100644 index 844f023..0000000 --- a/proto2ros_tests/test/generated/Diagnostic.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Generic diagnostic message. - -# Diagnostic severity. -proto2ros_tests/DiagnosticSeverity severity -# Diagnostic reason. -string reason -# Diagnostic attributes. -proto2ros_tests/DiagnosticAttributesEntry[] attributes diff --git a/proto2ros_tests/test/generated/DiagnosticAttributesEntry.msg b/proto2ros_tests/test/generated/DiagnosticAttributesEntry.msg deleted file mode 100644 index 49e0a1d..0000000 --- a/proto2ros_tests/test/generated/DiagnosticAttributesEntry.msg +++ /dev/null @@ -1,3 +0,0 @@ - -string key -string value diff --git a/proto2ros_tests/test/generated/DiagnosticSeverity.msg b/proto2ros_tests/test/generated/DiagnosticSeverity.msg deleted file mode 100644 index a3b5b2d..0000000 --- a/proto2ros_tests/test/generated/DiagnosticSeverity.msg +++ /dev/null @@ -1,7 +0,0 @@ - -# Informational diagnostic severity. -int32 INFO=0 -int32 WARN=1 -int32 FATAL=2 - -int32 value diff --git a/proto2ros_tests/test/generated/Dict.msg b/proto2ros_tests/test/generated/Dict.msg deleted file mode 100644 index c8af7cb..0000000 --- a/proto2ros_tests/test/generated/Dict.msg +++ /dev/null @@ -1,3 +0,0 @@ -# Heterogeneous dict. - -proto2ros_tests/DictItemsEntry[] items diff --git a/proto2ros_tests/test/generated/DictItemsEntry.msg b/proto2ros_tests/test/generated/DictItemsEntry.msg deleted file mode 100644 index 135266b..0000000 --- a/proto2ros_tests/test/generated/DictItemsEntry.msg +++ /dev/null @@ -1,3 +0,0 @@ - -string key -proto2ros/Any value # is proto2ros_tests/Value (type-erased) diff --git a/proto2ros_tests/test/generated/Direction.msg b/proto2ros_tests/test/generated/Direction.msg deleted file mode 100644 index f83636d..0000000 --- a/proto2ros_tests/test/generated/Direction.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Body relative directions for motion. - -int32 LEFT=0 -int32 RIGHT=1 -int32 FORWARD=2 -int32 BACKWARD=3 - -int32 value diff --git a/proto2ros_tests/test/generated/Displacement.msg b/proto2ros_tests/test/generated/Displacement.msg deleted file mode 100644 index 44d47ea..0000000 --- a/proto2ros_tests/test/generated/Displacement.msg +++ /dev/null @@ -1,6 +0,0 @@ -# 3D displacement - -uint8 TRANSLATION_FIELD_SET=1 - -geometry_msgs/Vector3 translation -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/Error.msg b/proto2ros_tests/test/generated/Error.msg deleted file mode 100644 index 1eca188..0000000 --- a/proto2ros_tests/test/generated/Error.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Generic error message. - -# Integer error code. -int32 code -# error reason. -string reason diff --git a/proto2ros_tests/test/generated/Fragment.msg b/proto2ros_tests/test/generated/Fragment.msg deleted file mode 100644 index fbb50e3..0000000 --- a/proto2ros_tests/test/generated/Fragment.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Binary recursive fragment. - -# fragment payload. -uint8[] payload -# nested fragments. -proto2ros/Any[] nested # is proto2ros_tests/Fragment[] (type-erased) diff --git a/proto2ros_tests/test/generated/Goal.msg b/proto2ros_tests/test/generated/Goal.msg deleted file mode 100644 index b42f910..0000000 --- a/proto2ros_tests/test/generated/Goal.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Generalized goal. - -uint8 TARGET_FIELD_SET=1 -uint8 ROI_FIELD_SET=2 - -geometry_msgs/Pose target -proto2ros/Any roi -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/HTTP.msg b/proto2ros_tests/test/generated/HTTP.msg deleted file mode 100644 index e8c00fa..0000000 --- a/proto2ros_tests/test/generated/HTTP.msg +++ /dev/null @@ -1,3 +0,0 @@ -# Empty HTTP message. -# -# For namespacing purposes only. diff --git a/proto2ros_tests/test/generated/HTTPMethod.msg b/proto2ros_tests/test/generated/HTTPMethod.msg deleted file mode 100644 index e407ae2..0000000 --- a/proto2ros_tests/test/generated/HTTPMethod.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Supported HTTP methods. - -int32 GET=0 -int32 HEAD=1 -int32 POST=2 -int32 PUT=3 - -int32 value diff --git a/proto2ros_tests/test/generated/HTTPRequest.msg b/proto2ros_tests/test/generated/HTTPRequest.msg deleted file mode 100644 index beb6fa1..0000000 --- a/proto2ros_tests/test/generated/HTTPRequest.msg +++ /dev/null @@ -1,8 +0,0 @@ -# HTTP request message. - -# HTTP request method. -proto2ros_tests/HTTPMethod method -# HTTP resource URI. -string uri -# HTTP request body. -uint8[] body diff --git a/proto2ros_tests/test/generated/HTTPResponse.msg b/proto2ros_tests/test/generated/HTTPResponse.msg deleted file mode 100644 index 7ab8e70..0000000 --- a/proto2ros_tests/test/generated/HTTPResponse.msg +++ /dev/null @@ -1,8 +0,0 @@ -# HTTP response message. - -# HTTP response status. -proto2ros_tests/HTTPStatus status -# HTTP response reason. -string reason -# HTTP response body. -uint8[] body diff --git a/proto2ros_tests/test/generated/HTTPStatus.msg b/proto2ros_tests/test/generated/HTTPStatus.msg deleted file mode 100644 index 8f7755b..0000000 --- a/proto2ros_tests/test/generated/HTTPStatus.msg +++ /dev/null @@ -1,8 +0,0 @@ -# HTTP status codes. - -int32 UNDEFINED=0 -int32 OK=200 -int32 NOT_FOUND=404 -int32 INTERNAL_SERVER_ERROR=500 - -int32 value diff --git a/proto2ros_tests/test/generated/HVACControlRequest.msg b/proto2ros_tests/test/generated/HVACControlRequest.msg deleted file mode 100644 index 770a3c7..0000000 --- a/proto2ros_tests/test/generated/HVACControlRequest.msg +++ /dev/null @@ -1,7 +0,0 @@ -# A control request for an HVAC system. - -uint8 TEMPERATURE_SETPOINT_FIELD_SET=2 - -float64 air_flow_rate -sensor_msgs/Temperature temperature_setpoint -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/List.msg b/proto2ros_tests/test/generated/List.msg deleted file mode 100644 index 717f239..0000000 --- a/proto2ros_tests/test/generated/List.msg +++ /dev/null @@ -1,4 +0,0 @@ -# Heterogeneous list. - -# Listed values. -proto2ros/Any[] values # is proto2ros_tests/Value[] (type-erased) diff --git a/proto2ros_tests/test/generated/Map.msg b/proto2ros_tests/test/generated/Map.msg deleted file mode 100644 index 90bfa43..0000000 --- a/proto2ros_tests/test/generated/Map.msg +++ /dev/null @@ -1,3 +0,0 @@ -# Generic map message. - -proto2ros_tests/MapSubmapsEntry[] submaps diff --git a/proto2ros_tests/test/generated/MapFragment.msg b/proto2ros_tests/test/generated/MapFragment.msg deleted file mode 100644 index a7fdd7a..0000000 --- a/proto2ros_tests/test/generated/MapFragment.msg +++ /dev/null @@ -1,5 +0,0 @@ - -int32 width -int32 height -# Fragment grid as a column-major matrix. -uint8[] grid diff --git a/proto2ros_tests/test/generated/MapSubmapsEntry.msg b/proto2ros_tests/test/generated/MapSubmapsEntry.msg deleted file mode 100644 index d92cac4..0000000 --- a/proto2ros_tests/test/generated/MapSubmapsEntry.msg +++ /dev/null @@ -1,3 +0,0 @@ - -int32 key -proto2ros_tests/MapFragment value diff --git a/proto2ros_tests/test/generated/Matrix.msg b/proto2ros_tests/test/generated/Matrix.msg deleted file mode 100644 index 79ed701..0000000 --- a/proto2ros_tests/test/generated/Matrix.msg +++ /dev/null @@ -1,5 +0,0 @@ - -uint32 rows -uint32 cols -# Row-major matrix data. -float64[] data diff --git a/proto2ros_tests/test/generated/MotionRequest.msg b/proto2ros_tests/test/generated/MotionRequest.msg deleted file mode 100644 index a4bab33..0000000 --- a/proto2ros_tests/test/generated/MotionRequest.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Discrete motion request. - -# Direction of motion. -proto2ros_tests/MotionRequestDirection direction -# motion speed. -float32 speed diff --git a/proto2ros_tests/test/generated/MotionRequestDirection.msg b/proto2ros_tests/test/generated/MotionRequestDirection.msg deleted file mode 100644 index 664fc93..0000000 --- a/proto2ros_tests/test/generated/MotionRequestDirection.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Valid directions for motion. - -int32 LEFT=0 -int32 RIGHT=1 -int32 FORWARD=2 -int32 BACKWARD=3 - -int32 value diff --git a/proto2ros_tests/test/generated/Pair.msg b/proto2ros_tests/test/generated/Pair.msg deleted file mode 100644 index 64fc5f7..0000000 --- a/proto2ros_tests/test/generated/Pair.msg +++ /dev/null @@ -1,10 +0,0 @@ -# Heterogeneous pair. - -uint8 FIRST_FIELD_SET=1 -uint8 SECOND_FIELD_SET=2 - -# First value. -proto2ros/Any first # is proto2ros_tests/Value (type-erased) -# Second value. -proto2ros/Any second # is proto2ros_tests/Value (type-erased) -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/RTTIQueryRequest.msg b/proto2ros_tests/test/generated/RTTIQueryRequest.msg deleted file mode 100644 index f762e1e..0000000 --- a/proto2ros_tests/test/generated/RTTIQueryRequest.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Protobuf type query request message. - -uint8 MSG_FIELD_SET=1 - -proto2ros/AnyProto msg -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/RTTIQueryResult.msg b/proto2ros_tests/test/generated/RTTIQueryResult.msg deleted file mode 100644 index 8244044..0000000 --- a/proto2ros_tests/test/generated/RTTIQueryResult.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Protobuf type query result message. - -uint8 TYPE_FIELD_SET=1 - -proto2ros/AnyProto type -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/RemoteExecutionRequest.msg b/proto2ros_tests/test/generated/RemoteExecutionRequest.msg deleted file mode 100644 index 6dff7ca..0000000 --- a/proto2ros_tests/test/generated/RemoteExecutionRequest.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Remote RPA request. - -# Execution prompt. -string prompt -# Timeout for request, in nanoseconds -int64 timeout # deprecated -# Timeout for request, in seconds. -float32 timeout_sec diff --git a/proto2ros_tests/test/generated/RemoteExecutionResult.msg b/proto2ros_tests/test/generated/RemoteExecutionResult.msg deleted file mode 100644 index 4039b7d..0000000 --- a/proto2ros_tests/test/generated/RemoteExecutionResult.msg +++ /dev/null @@ -1,7 +0,0 @@ -# Remote RPA result. - -uint8 ERROR_FIELD_SET=2 - -bool ok -proto2ros_tests/RemoteExecutionResultError error -uint8 has_field 255 diff --git a/proto2ros_tests/test/generated/RemoteExecutionResultError.msg b/proto2ros_tests/test/generated/RemoteExecutionResultError.msg deleted file mode 100644 index a590526..0000000 --- a/proto2ros_tests/test/generated/RemoteExecutionResultError.msg +++ /dev/null @@ -1,6 +0,0 @@ -# Remote execution error. - -int32 code -# May contain backslashes (). -string reason -string[] traceback diff --git a/proto2ros_tests/test/generated/Value.msg b/proto2ros_tests/test/generated/Value.msg deleted file mode 100644 index bbe4c98..0000000 --- a/proto2ros_tests/test/generated/Value.msg +++ /dev/null @@ -1,3 +0,0 @@ -# Heterogeneous value. - -proto2ros_tests/ValueOneOfData data diff --git a/proto2ros_tests/test/generated/ValueOneOfData.msg b/proto2ros_tests/test/generated/ValueOneOfData.msg deleted file mode 100644 index 741b34c..0000000 --- a/proto2ros_tests/test/generated/ValueOneOfData.msg +++ /dev/null @@ -1,20 +0,0 @@ - -int8 DATA_NOT_SET=0 -int8 DATA_NUMBER_SET=1 -int8 DATA_TEXT_SET=2 -int8 DATA_PAIR_SET=3 -int8 DATA_LIST_SET=4 -int8 DATA_DICT_SET=5 - -# Numeric value. -float32 number -# Text value. -string text -# Pair value. -proto2ros_tests/Pair pair -# List value. -proto2ros_tests/List list -# Dict value. -proto2ros_tests/Dict dict -int8 data_choice # deprecated -int8 which diff --git a/proto2ros_tests/test/test_proto2ros.cpp b/proto2ros_tests/test/test_proto2ros.cpp deleted file mode 100644 index 7335a0d..0000000 --- a/proto2ros_tests/test/test_proto2ros.cpp +++ /dev/null @@ -1,465 +0,0 @@ -// Copyright (c) 2024 Boston Dynamics AI Institute LLC. All rights reserved. -// NOLINTBEGIN(readability-magic-numbers,readability-function-cognitive-complexity,cppcoreguidelines-pro-type-reinterpret-cast) -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using proto2ros::conversions::Convert; -using proto2ros_tests::conversions::Convert; - -TEST(Proto2RosTesting, MessageMapping) { - auto proto_request = proto2ros_tests::HVACControlRequest(); - proto_request.set_air_flow_rate(1000.0); - auto* setpoint = proto_request.mutable_temperature_setpoint(); - setpoint->set_value(77.0); - setpoint->set_scale(proto2ros_tests::Temperature::FAHRENHEIT); - - auto ros_request = proto2ros_tests::msg::HVACControlRequest(); - Convert(proto_request, &ros_request); - - auto other_proto_request = proto2ros_tests::HVACControlRequest(); - Convert(ros_request, &other_proto_request); - - EXPECT_DOUBLE_EQ(proto_request.air_flow_rate(), other_proto_request.air_flow_rate()); - // sensor_msgs/Temperature messages are in the Celsius temperature scale - EXPECT_DOUBLE_EQ(other_proto_request.temperature_setpoint().value(), 25.0); - EXPECT_EQ(other_proto_request.temperature_setpoint().scale(), proto2ros_tests::Temperature::CELSIUS); -} - -TEST(Proto2RosTesting, RecursiveMessages) { - auto proto_fragment = proto2ros_tests::Fragment(); - proto_fragment.set_payload("important data"); - auto* proto_subfragment = proto_fragment.add_nested(); - proto_subfragment->set_payload("important addendum"); - - auto ros_fragment = proto2ros_tests::msg::Fragment(); - Convert(proto_fragment, &ros_fragment); - - const auto payload = - std::string_view{reinterpret_cast(ros_fragment.payload.data()), ros_fragment.payload.size()}; - EXPECT_EQ(payload, proto_fragment.payload()); - EXPECT_EQ(ros_fragment.nested.size(), static_cast(proto_fragment.nested_size())); - - auto other_proto_fragment = proto2ros_tests::Fragment(); - Convert(ros_fragment, &other_proto_fragment); - EXPECT_EQ(other_proto_fragment.payload(), proto_fragment.payload()); - ASSERT_EQ(other_proto_fragment.nested_size(), proto_fragment.nested_size()); - const auto& other_proto_subfragment = other_proto_fragment.nested(0); - EXPECT_EQ(other_proto_subfragment.payload(), proto_subfragment->payload()); -} - -TEST(Proto2RosTesting, CircularlyDependentMessages) { - auto proto_value = proto2ros_tests::Value(); - auto* proto_items = proto_value.mutable_dict()->mutable_items(); - auto* proto_pair = (*proto_items)["interval"].mutable_pair(); - proto_pair->mutable_first()->set_number(-0.5F); - proto_pair->mutable_second()->set_number(0.5F); - auto* proto_list = (*proto_items)["range"].mutable_list(); - for (const float number : {-0.1F, 0.0F, 0.1F, -0.7F, 0.3F, 0.4F}) { - proto_list->add_values()->set_number(number); - } - - auto ros_value = proto2ros_tests::msg::Value(); - Convert(proto_value, &ros_value); - - auto other_proto_value = proto2ros_tests::Value(); - Convert(ros_value, &other_proto_value); - - const auto& other_proto_items = other_proto_value.dict().items(); - EXPECT_TRUE(other_proto_items.count("interval")); - const auto& other_proto_pair = other_proto_items.at("interval").pair(); - EXPECT_EQ(other_proto_pair.first().number(), proto_pair->first().number()); - EXPECT_EQ(other_proto_pair.second().number(), proto_pair->second().number()); - EXPECT_TRUE(other_proto_items.count("range")); - const auto& other_proto_list = other_proto_items.at("range").list(); - ASSERT_EQ(other_proto_list.values_size(), proto_list->values_size()); - for (int i = 0; i < proto_list->values_size(); ++i) { - EXPECT_EQ(other_proto_list.values(i).number(), proto_list->values(i).number()); - } -} - -TEST(Proto2RosTesting, MessagesWithEnums) { - auto proto_motion_request = proto2ros_tests::MotionRequest(); - proto_motion_request.set_direction(proto2ros_tests::MotionRequest::Forward); - proto_motion_request.set_speed(1.0); - auto ros_motion_request = proto2ros_tests::msg::MotionRequest(); - Convert(proto_motion_request, &ros_motion_request); - EXPECT_EQ(ros_motion_request.direction.value, proto_motion_request.direction()); - EXPECT_EQ(ros_motion_request.speed, proto_motion_request.speed()); - - auto other_proto_motion_request = proto2ros_tests::MotionRequest(); - Convert(ros_motion_request, &other_proto_motion_request); - EXPECT_EQ(other_proto_motion_request.direction(), proto_motion_request.direction()); - EXPECT_EQ(other_proto_motion_request.speed(), proto_motion_request.speed()); -} - -TEST(Proto2RosTesting, MessagesWithNestedEnums) { - auto proto_http_request = proto2ros_tests::HTTP::Request(); - proto_http_request.set_method(proto2ros_tests::HTTP::PUT); - proto_http_request.set_uri("https://proto2ros.xyz/post"); - - auto ros_http_request = proto2ros_tests::msg::HTTPRequest(); - Convert(proto_http_request, &ros_http_request); - EXPECT_EQ(ros_http_request.method.value, proto_http_request.method()); - EXPECT_EQ(ros_http_request.uri, proto_http_request.uri()); - - auto other_proto_http_request = proto2ros_tests::HTTP::Request(); - Convert(ros_http_request, &other_proto_http_request); - EXPECT_EQ(other_proto_http_request.method(), proto_http_request.method()); - EXPECT_EQ(other_proto_http_request.uri(), proto_http_request.uri()); -} - -TEST(Proto2RosTesting, OneOfMessages) { - auto proto_any_command = proto2ros_tests::AnyCommand(); - proto_any_command.mutable_walk()->set_distance(1.0); - proto_any_command.mutable_walk()->set_speed(1.0); - - auto ros_any_command = proto2ros_tests::msg::AnyCommand(); - Convert(proto_any_command, &ros_any_command); - constexpr auto expected_choice = proto2ros_tests::msg::AnyCommandOneOfCommands::COMMANDS_WALK_SET; - EXPECT_EQ(ros_any_command.commands.which, expected_choice); - EXPECT_EQ(ros_any_command.commands.commands_choice, expected_choice); - EXPECT_EQ(ros_any_command.commands.walk.distance, proto_any_command.walk().distance()); - EXPECT_EQ(ros_any_command.commands.walk.speed, proto_any_command.walk().speed()); - - auto other_proto_any_command = proto2ros_tests::AnyCommand(); - Convert(ros_any_command, &other_proto_any_command); - EXPECT_TRUE(other_proto_any_command.has_walk()); - EXPECT_EQ(other_proto_any_command.walk().distance(), proto_any_command.walk().distance()); - EXPECT_EQ(other_proto_any_command.walk().speed(), proto_any_command.walk().speed()); -} - -TEST(Proto2RosTesting, OneOfEmptyMessages) { - auto proto_any_command = proto2ros_tests::AnyCommand(); - (void)proto_any_command.mutable_sit(); - - auto ros_any_command = proto2ros_tests::msg::AnyCommand(); - Convert(proto_any_command, &ros_any_command); - - constexpr auto expected_choice = proto2ros_tests::msg::AnyCommandOneOfCommands::COMMANDS_SIT_SET; - EXPECT_EQ(ros_any_command.commands.which, expected_choice); - EXPECT_EQ(ros_any_command.commands.commands_choice, expected_choice); - - auto other_proto_any_command = proto2ros_tests::AnyCommand(); - Convert(ros_any_command, &other_proto_any_command); - EXPECT_TRUE(other_proto_any_command.has_sit()); -} - -TEST(Proto2RosTesting, MessagesWithMapField) { - auto proto_diagnostic = proto2ros_tests::Diagnostic(); - proto_diagnostic.set_severity(proto2ros_tests::Diagnostic::FATAL); - proto_diagnostic.set_reason("Localization estimate diverged, cannot recover"); - auto& proto_attributes = *proto_diagnostic.mutable_attributes(); - proto_attributes["origin"] = "localization subsystem"; - - auto ros_diagnostic = proto2ros_tests::msg::Diagnostic(); - Convert(proto_diagnostic, &ros_diagnostic); - EXPECT_EQ(ros_diagnostic.severity.value, proto_diagnostic.severity()); - EXPECT_EQ(ros_diagnostic.reason, proto_diagnostic.reason()); - EXPECT_EQ(ros_diagnostic.reason, proto_diagnostic.reason()); - EXPECT_EQ(ros_diagnostic.attributes.size(), static_cast(proto_attributes.size())); - EXPECT_EQ(ros_diagnostic.attributes[0].key, "origin"); - EXPECT_EQ(ros_diagnostic.attributes[0].value, "localization subsystem"); - - auto other_proto_diagnostic = proto2ros_tests::Diagnostic(); - Convert(ros_diagnostic, &other_proto_diagnostic); - EXPECT_EQ(other_proto_diagnostic.severity(), proto_diagnostic.severity()); - EXPECT_EQ(other_proto_diagnostic.reason(), proto_diagnostic.reason()); - EXPECT_TRUE(other_proto_diagnostic.attributes().contains("origin")); - EXPECT_EQ(other_proto_diagnostic.attributes().at("origin"), "localization subsystem"); -} - -TEST(Proto2RosTesting, MessagesWithDeprecatedFields) { - auto proto_remote_execution_request = proto2ros_tests::RemoteExecutionRequest(); - proto_remote_execution_request.set_prompt("grab bike seat"); - proto_remote_execution_request.set_timeout(10000000000); - proto_remote_execution_request.set_timeout_sec(10.0); - - auto ros_remote_execution_request = proto2ros_tests::msg::RemoteExecutionRequest(); - Convert(proto_remote_execution_request, &ros_remote_execution_request); - EXPECT_EQ(ros_remote_execution_request.prompt, proto_remote_execution_request.prompt()); - EXPECT_EQ(ros_remote_execution_request.timeout, proto_remote_execution_request.timeout()); - EXPECT_EQ(ros_remote_execution_request.timeout_sec, proto_remote_execution_request.timeout_sec()); - - auto other_proto_remote_execution_request = proto2ros_tests::RemoteExecutionRequest(); - Convert(ros_remote_execution_request, &other_proto_remote_execution_request); - EXPECT_EQ(other_proto_remote_execution_request.prompt(), proto_remote_execution_request.prompt()); - EXPECT_EQ(other_proto_remote_execution_request.timeout(), proto_remote_execution_request.timeout()); - EXPECT_EQ(other_proto_remote_execution_request.timeout_sec(), proto_remote_execution_request.timeout_sec()); -} - -TEST(Proto2RosTesting, RedefinedMessages) { - auto proto_remote_execution_result = proto2ros_tests::RemoteExecutionResult(); - proto_remote_execution_result.set_ok(false); - auto* proto_remote_execution_error = proto_remote_execution_result.mutable_error(); - proto_remote_execution_error->set_code(255); - proto_remote_execution_error->set_reason("interrupted"); - proto_remote_execution_error->add_traceback(""); - - auto ros_remote_execution_result = proto2ros_tests::msg::RemoteExecutionResult(); - Convert(proto_remote_execution_result, &ros_remote_execution_result); - EXPECT_EQ(ros_remote_execution_result.ok, proto_remote_execution_result.ok()); - EXPECT_EQ(ros_remote_execution_result.error.code, proto_remote_execution_result.error().code()); - EXPECT_EQ(ros_remote_execution_result.error.reason, proto_remote_execution_result.error().reason()); - EXPECT_EQ(ros_remote_execution_result.error.traceback.size(), - static_cast(proto_remote_execution_error->traceback_size())); - EXPECT_EQ(ros_remote_execution_result.error.traceback[0], proto_remote_execution_result.error().traceback(0)); - - auto other_proto_remote_execution_result = proto2ros_tests::RemoteExecutionResult(); - Convert(ros_remote_execution_result, &other_proto_remote_execution_result); - EXPECT_EQ(other_proto_remote_execution_result.ok(), proto_remote_execution_result.ok()); - EXPECT_EQ(other_proto_remote_execution_result.error().code(), proto_remote_execution_result.error().code()); - EXPECT_EQ(other_proto_remote_execution_result.error().reason(), proto_remote_execution_result.error().reason()); - EXPECT_EQ(other_proto_remote_execution_result.error().traceback_size(), - proto_remote_execution_error->traceback_size()); - EXPECT_EQ(other_proto_remote_execution_result.error().traceback(0), - proto_remote_execution_result.error().traceback(0)); -} - -namespace { -// NOLINTBEGIN(readability-identifier-naming) -template -struct has_rotation_member : std::false_type {}; - -// specialization recognizes types that do have a nested ::type member: -template -struct has_rotation_member().rotation)>> : std::true_type {}; - -template -constexpr bool has_rotation_member_v = has_rotation_member::value; -// NOLINTEND(readability-identifier-naming) -} // namespace - -TEST(Proto2RosTesting, MessagesWithReservedFields) { - auto proto_displacement = proto2ros_tests::Displacement(); - proto_displacement.mutable_translation()->set_x(1.0); - proto_displacement.mutable_translation()->set_y(2.0); - proto_displacement.mutable_translation()->set_z(3.0); - - auto ros_displacement = proto2ros_tests::msg::Displacement(); - Convert(proto_displacement, &ros_displacement); - EXPECT_EQ(ros_displacement.translation.x, proto_displacement.translation().x()); - EXPECT_EQ(ros_displacement.translation.y, proto_displacement.translation().y()); - EXPECT_EQ(ros_displacement.translation.z, proto_displacement.translation().z()); - static_assert(!has_rotation_member_v); - - auto other_proto_displacement = proto2ros_tests::Displacement(); - Convert(ros_displacement, &other_proto_displacement); - EXPECT_EQ(other_proto_displacement.translation().x(), proto_displacement.translation().x()); - EXPECT_EQ(other_proto_displacement.translation().y(), proto_displacement.translation().y()); - EXPECT_EQ(other_proto_displacement.translation().z(), proto_displacement.translation().z()); -} - -TEST(Proto2RosTesting, MessageForwarding) { - auto proto_camera_info = proto2ros_tests::CameraInfo(); - proto_camera_info.set_height(720); - proto_camera_info.set_width(1280); - proto_camera_info.mutable_k()->set_rows(3); - proto_camera_info.mutable_k()->set_cols(3); - for (const double number : {2000.0, 0.0, 800.0, 0.0, 2000.0, 800.0, 0.0, 0.0, 1.0}) { - proto_camera_info.mutable_k()->add_data(number); - } - proto_camera_info.mutable_r()->set_rows(3); - proto_camera_info.mutable_r()->set_cols(3); - for (const double number : {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}) { - proto_camera_info.mutable_r()->add_data(number); - } - proto_camera_info.mutable_p()->set_rows(3); - proto_camera_info.mutable_p()->set_cols(4); - for (const double number : {2000.0, 0.0, 800.0, 0.0, 0.0, 2000.0, 800.0, 0.0, 0.0, 0.0, 0.0, 1.0}) { - proto_camera_info.mutable_p()->add_data(number); - } - proto_camera_info.mutable_distortion_model()->set_type(proto2ros_tests::CameraInfo::DistortionModel::PLUMB_BOB); - for (const double coeff : {-0.2, -0.4, -0.0001, -0.0001, 0.0}) { - proto_camera_info.mutable_distortion_model()->add_coefficients(coeff); - } - - auto ros_camera_info = proto2ros_tests::msg::CameraInfo(); - Convert(proto_camera_info, &ros_camera_info); - EXPECT_EQ(ros_camera_info.height, proto_camera_info.height()); - EXPECT_EQ(ros_camera_info.width, proto_camera_info.width()); - EXPECT_EQ(ros_camera_info.k.rows, proto_camera_info.k().rows()); - EXPECT_EQ(ros_camera_info.k.cols, proto_camera_info.k().cols()); - EXPECT_EQ(ros_camera_info.k.data.size(), static_cast(proto_camera_info.k().data_size())); - for (size_t i = 0; i < ros_camera_info.k.data.size(); ++i) { - EXPECT_EQ(ros_camera_info.k.data[i], proto_camera_info.k().data(i)); - } - EXPECT_EQ(ros_camera_info.r.rows, proto_camera_info.r().rows()); - EXPECT_EQ(ros_camera_info.r.cols, proto_camera_info.r().cols()); - EXPECT_EQ(ros_camera_info.r.data.size(), static_cast(proto_camera_info.r().data_size())); - for (size_t i = 0; i < ros_camera_info.r.data.size(); ++i) { - EXPECT_EQ(ros_camera_info.r.data[i], proto_camera_info.r().data(i)); - } - EXPECT_EQ(ros_camera_info.p.rows, proto_camera_info.p().rows()); - EXPECT_EQ(ros_camera_info.p.cols, proto_camera_info.p().cols()); - EXPECT_EQ(ros_camera_info.p.data.size(), static_cast(proto_camera_info.p().data_size())); - for (size_t i = 0; i < ros_camera_info.p.data.size(); ++i) { - EXPECT_EQ(ros_camera_info.p.data[i], proto_camera_info.p().data(i)); - } - EXPECT_EQ(ros_camera_info.distortion_model.type.value, proto_camera_info.distortion_model().type()); - EXPECT_EQ(ros_camera_info.distortion_model.coefficients.size(), - static_cast(proto_camera_info.distortion_model().coefficients_size())); - for (size_t i = 0; i < ros_camera_info.distortion_model.coefficients.size(); ++i) { - EXPECT_EQ(ros_camera_info.distortion_model.coefficients[i], proto_camera_info.distortion_model().coefficients(i)); - } - - auto other_proto_camera_info = proto2ros_tests::CameraInfo(); - Convert(ros_camera_info, &other_proto_camera_info); - EXPECT_EQ(other_proto_camera_info.height(), proto_camera_info.height()); - EXPECT_EQ(other_proto_camera_info.width(), proto_camera_info.width()); - EXPECT_EQ(other_proto_camera_info.k().rows(), proto_camera_info.k().rows()); - EXPECT_EQ(other_proto_camera_info.k().cols(), proto_camera_info.k().cols()); - EXPECT_EQ(other_proto_camera_info.k().data_size(), proto_camera_info.k().data_size()); - for (int i = 0; i < other_proto_camera_info.k().data_size(); ++i) { - EXPECT_EQ(other_proto_camera_info.k().data(i), proto_camera_info.k().data(i)); - } - EXPECT_EQ(other_proto_camera_info.r().rows(), proto_camera_info.r().rows()); - EXPECT_EQ(other_proto_camera_info.r().cols(), proto_camera_info.r().cols()); - EXPECT_EQ(other_proto_camera_info.r().data_size(), proto_camera_info.r().data_size()); - for (int i = 0; i < other_proto_camera_info.r().data_size(); ++i) { - EXPECT_EQ(other_proto_camera_info.r().data(i), proto_camera_info.r().data(i)); - } - EXPECT_EQ(other_proto_camera_info.p().rows(), proto_camera_info.p().rows()); - EXPECT_EQ(other_proto_camera_info.p().cols(), proto_camera_info.p().cols()); - EXPECT_EQ(other_proto_camera_info.p().data_size(), proto_camera_info.p().data_size()); - for (int i = 0; i < other_proto_camera_info.p().data_size(); ++i) { - EXPECT_EQ(other_proto_camera_info.p().data(i), proto_camera_info.p().data(i)); - } - EXPECT_EQ(other_proto_camera_info.distortion_model().type(), proto_camera_info.distortion_model().type()); - EXPECT_EQ(other_proto_camera_info.distortion_model().coefficients_size(), - proto_camera_info.distortion_model().coefficients_size()); - for (int i = 0; i < other_proto_camera_info.distortion_model().coefficients_size(); ++i) { - EXPECT_EQ(other_proto_camera_info.distortion_model().coefficients(i), - proto_camera_info.distortion_model().coefficients(i)); - } -} - -TEST(Proto2RosTesting, MessagesWithSubMessageMapField) { - auto proto_map = proto2ros_tests::Map(); - auto& proto_submaps = *proto_map.mutable_submaps(); - auto& proto_fragment = proto_submaps[13]; - proto_fragment.set_height(20); - proto_fragment.set_width(20); - auto* proto_grid = proto_fragment.mutable_grid(); - proto_grid->resize(20UL * 20UL, '\0'); - - auto ros_map = proto2ros_tests::msg::Map(); - Convert(proto_map, &ros_map); - EXPECT_EQ(ros_map.submaps.size(), static_cast(proto_submaps.size())); - EXPECT_EQ(ros_map.submaps[0].key, 13); - auto& ros_fragment = ros_map.submaps[0].value; - EXPECT_EQ(ros_fragment.height, proto_fragment.height()); - EXPECT_EQ(ros_fragment.width, proto_fragment.width()); - EXPECT_EQ(ros_fragment.grid.size(), static_cast(proto_grid->size())); - const auto grid = std::string_view{reinterpret_cast(ros_fragment.grid.data()), ros_fragment.grid.size()}; - EXPECT_EQ(grid, proto_fragment.grid()); - - auto other_proto_map = proto2ros_tests::Map(); - Convert(ros_map, &other_proto_map); - EXPECT_TRUE(other_proto_map.submaps().contains(13)); - const auto& other_proto_fragment = other_proto_map.submaps().at(13); - EXPECT_EQ(other_proto_fragment.height(), proto_fragment.height()); - EXPECT_EQ(other_proto_fragment.width(), proto_fragment.width()); - EXPECT_EQ(other_proto_fragment.grid(), proto_fragment.grid()); -} - -TEST(Proto2RosTesting, MessagesWithAnyFields) { - auto proto_matrix = proto2ros_tests::Matrix(); - proto_matrix.set_rows(1); - proto_matrix.set_cols(1); - proto_matrix.add_data(0.0); - - auto proto_request = proto2ros_tests::RTTIQueryRequest(); - proto_request.mutable_msg()->PackFrom(proto_matrix); - - auto ros_request = proto2ros_tests::msg::RTTIQueryRequest(); - Convert(proto_request, &ros_request); - - auto other_proto_request = proto2ros_tests::RTTIQueryRequest(); - Convert(ros_request, &other_proto_request); - - auto other_unpacked_proto_matrix = proto2ros_tests::Matrix(); - EXPECT_TRUE(other_proto_request.msg().UnpackTo(&other_unpacked_proto_matrix)); - - EXPECT_EQ(other_unpacked_proto_matrix.rows(), proto_matrix.rows()); - EXPECT_EQ(other_unpacked_proto_matrix.cols(), proto_matrix.cols()); - EXPECT_EQ(other_unpacked_proto_matrix.data_size(), proto_matrix.data_size()); - EXPECT_EQ(other_unpacked_proto_matrix.data(0), proto_matrix.data(0)); -} - -TEST(Proto2RosTesting, MessagesWithUnknownTypeFields) { - auto proto_result = proto2ros_tests::RTTIQueryResult(); - proto_result.mutable_type()->set_name("SomeMessage"); - - auto ros_result = proto2ros_tests::msg::RTTIQueryResult(); - Convert(proto_result, &ros_result); - - auto unpacked_proto_type = google::protobuf::Type(); - Convert(ros_result.type, &unpacked_proto_type); - EXPECT_EQ(unpacked_proto_type.name(), proto_result.type().name()); - - auto other_proto_result = proto2ros_tests::RTTIQueryResult(); - Convert(ros_result, &other_proto_result); - EXPECT_EQ(other_proto_result.type().name(), proto_result.type().name()); -} - -TEST(Proto2RosTesting, MessagesWithExpandedAnyFields) { - auto proto_target = bosdyn::api::SE2Pose(); - proto_target.mutable_position()->set_x(1.0); - proto_target.mutable_position()->set_y(-1.0); - proto_target.set_angle(M_PI); - - auto proto_roi = bosdyn::api::Polygon(); - for (const double dx : {-0.5, 0.5}) { - for (const double dy : {-0.5, 0.5}) { - auto* vertex = proto_roi.add_vertexes(); - vertex->set_x(proto_target.position().x() + dx); - vertex->set_y(proto_target.position().y() + dy); - } - } - - auto proto_goal = proto2ros_tests::Goal(); - proto_goal.mutable_target()->PackFrom(proto_target); - proto_goal.mutable_roi()->PackFrom(proto_roi); - - auto ros_goal = proto2ros_tests::msg::Goal(); - Convert(proto_goal, &ros_goal); - - EXPECT_EQ(ros_goal.target.position.x, proto_target.position().x()); - EXPECT_EQ(ros_goal.target.position.y, proto_target.position().y()); - EXPECT_EQ(ros_goal.roi.type_name, "geometry_msgs/Polygon"); - - auto other_proto_goal = proto2ros_tests::Goal(); - Convert(ros_goal, &other_proto_goal); - - auto other_proto_target = bosdyn::api::SE2Pose(); - EXPECT_TRUE(other_proto_goal.target().UnpackTo(&other_proto_target)); - - EXPECT_EQ(proto_target.position().x(), other_proto_target.position().x()); - EXPECT_EQ(proto_target.position().y(), other_proto_target.position().y()); - EXPECT_EQ(proto_target.angle(), other_proto_target.angle()); - - auto other_proto_roi = bosdyn::api::Polygon(); - EXPECT_TRUE(other_proto_goal.roi().UnpackTo(&other_proto_roi)); - EXPECT_EQ(other_proto_roi.vertexes_size(), proto_roi.vertexes_size()); - for (int i = 0; i < proto_roi.vertexes_size(); ++i) { - EXPECT_EQ(other_proto_roi.vertexes(i).x(), proto_roi.vertexes(i).x()); - EXPECT_EQ(other_proto_roi.vertexes(i).y(), proto_roi.vertexes(i).y()); - } -} -// NOLINTEND(readability-magic-numbers,readability-function-cognitive-complexity,cppcoreguidelines-pro-type-reinterpret-cast) diff --git a/proto2ros_tests/test/test_proto2ros.py b/proto2ros_tests/test/test_proto2ros.py deleted file mode 100644 index 1530884..0000000 --- a/proto2ros_tests/test/test_proto2ros.py +++ /dev/null @@ -1,386 +0,0 @@ -# Copyright (c) 2023 Boston Dynamics AI Institute LLC. All rights reserved. - -import filecmp -import math -import os -import pathlib - -import bosdyn.api.geometry_pb2 -import geometry_msgs.msg -import google.protobuf.type_pb2 -import test_pb2 - -import proto2ros.msg -import proto2ros_tests.msg -from proto2ros_tests.conversions import convert - - -def test_message_generation() -> None: - cmake_binary_dir = pathlib.Path(os.environ["CMAKE_BINARY_DIR"]) - gen_msg_dir = cmake_binary_dir / "proto2ros_generate" / "proto2ros_tests" / "msg" - ref_msg_dir = pathlib.Path(__file__).resolve().parent / "generated" - _, mismatch, errors = filecmp.cmpfiles(gen_msg_dir, ref_msg_dir, os.listdir(ref_msg_dir)) - assert not mismatch, mismatch - assert not errors, errors - - -def test_message_mapping() -> None: - assert not hasattr(proto2ros_tests.msg, "Temperature") - assert not hasattr(proto2ros_tests.msg, "TemperatureScale") - proto_request = test_pb2.HVACControlRequest() - proto_request.air_flow_rate = 1000.0 - proto_request.temperature_setpoint.value = 77.0 - proto_request.temperature_setpoint.scale = test_pb2.Temperature.Scale.FAHRENHEIT - ros_request = proto2ros_tests.msg.HVACControlRequest() - convert(proto_request, ros_request) - other_proto_request = test_pb2.HVACControlRequest() - convert(ros_request, other_proto_request) - assert proto_request.air_flow_rate == other_proto_request.air_flow_rate - # sensor_msgs/Temperature messages are in the Celsius temperature scale - assert other_proto_request.temperature_setpoint.value == 25.0 - assert other_proto_request.temperature_setpoint.scale == test_pb2.Temperature.Scale.CELSIUS - - -def test_recursive_messages() -> None: - proto_fragment = test_pb2.Fragment() - proto_fragment.payload = b"important data" - proto_subfragment = test_pb2.Fragment() - proto_subfragment.payload = b"important addendum" - proto_fragment.nested.append(proto_subfragment) - ros_fragment = proto2ros_tests.msg.Fragment() - convert(proto_fragment, ros_fragment) - assert ros_fragment.payload.tobytes() == proto_fragment.payload - assert len(ros_fragment.nested) == len(proto_fragment.nested) - other_proto_fragment = test_pb2.Fragment() - convert(ros_fragment, other_proto_fragment) - assert other_proto_fragment.payload == proto_fragment.payload - assert len(other_proto_fragment.nested) == len(proto_fragment.nested) - other_proto_subfragment = other_proto_fragment.nested[0] - assert other_proto_subfragment.payload == proto_subfragment.payload - - -def test_circularly_dependent_messages() -> None: - proto_value = test_pb2.Value() - proto_pair_value = proto_value.dict.items["interval"] - proto_pair_value.pair.first.number = -0.5 - proto_pair_value.pair.second.number = 0.5 - proto_list_value = proto_value.dict.items["range"] - for number in (-0.1, 0.0, 0.1, -0.7, 0.3, 0.4): - value = proto_list_value.list.values.add() - value.number = number - ros_value = proto2ros_tests.msg.Value() - convert(proto_value, ros_value) - other_proto_value = test_pb2.Value() - convert(ros_value, other_proto_value) - assert "interval" in other_proto_value.dict.items - other_proto_pair_value = proto_value.dict.items["interval"] - assert other_proto_pair_value.pair.first.number == proto_pair_value.pair.first.number - assert other_proto_pair_value.pair.second.number == proto_pair_value.pair.second.number - other_proto_list_value = proto_value.dict.items["range"] - assert [v.number for v in other_proto_list_value.list.values] == [v.number for v in proto_list_value.list.values] - - -def test_messages_with_enums() -> None: - proto_motion_request = test_pb2.MotionRequest() - proto_motion_request.direction = test_pb2.MotionRequest.Direction.Forward - proto_motion_request.speed = 1.0 - ros_motion_request = proto2ros_tests.msg.MotionRequest() - convert(proto_motion_request, ros_motion_request) - assert ros_motion_request.direction.value == proto_motion_request.direction - assert ros_motion_request.speed == proto_motion_request.speed - other_proto_motion_request = test_pb2.MotionRequest() - convert(ros_motion_request, other_proto_motion_request) - assert other_proto_motion_request.direction == proto_motion_request.direction - assert other_proto_motion_request.speed == proto_motion_request.speed - - -def test_messages_with_nested_enums() -> None: - proto_http_request = test_pb2.HTTP.Request() - proto_http_request.method = test_pb2.HTTP.Method.PUT - proto_http_request.uri = "https://proto2ros.xyz/post" - ros_http_request = proto2ros_tests.msg.HTTPRequest() - convert(proto_http_request, ros_http_request) - assert ros_http_request.method.value == test_pb2.HTTP.Method.PUT - assert ros_http_request.uri == proto_http_request.uri - other_proto_http_request = test_pb2.HTTP.Request() - convert(ros_http_request, other_proto_http_request) - assert other_proto_http_request.method == proto_http_request.method - assert other_proto_http_request.uri == proto_http_request.uri - - -def test_one_of_messages() -> None: - proto_any_command = test_pb2.AnyCommand() - proto_any_command.walk.distance = 1.0 - proto_any_command.walk.speed = 1.0 - - ros_any_command = proto2ros_tests.msg.AnyCommand() - convert(proto_any_command, ros_any_command) - walk_set = proto2ros_tests.msg.AnyCommandOneOfCommands.COMMANDS_WALK_SET - assert ros_any_command.commands.which == walk_set - assert ros_any_command.commands.commands_choice == walk_set - assert ros_any_command.commands.walk.distance == proto_any_command.walk.distance - assert ros_any_command.commands.walk.speed == proto_any_command.walk.speed - - other_proto_any_command = test_pb2.AnyCommand() - convert(ros_any_command, other_proto_any_command) - assert other_proto_any_command.WhichOneof("commands") == "walk" - assert other_proto_any_command.walk.distance == proto_any_command.walk.distance - assert other_proto_any_command.walk.speed == proto_any_command.walk.speed - - -def test_one_of_empty_messages() -> None: - proto_any_command = test_pb2.AnyCommand() - proto_any_command.sit.SetInParent() - - ros_any_command = proto2ros_tests.msg.AnyCommand() - convert(proto_any_command, ros_any_command) - sit_set = proto2ros_tests.msg.AnyCommandOneOfCommands.COMMANDS_SIT_SET - assert ros_any_command.commands.which == sit_set - assert ros_any_command.commands.commands_choice == sit_set - - other_proto_any_command = test_pb2.AnyCommand() - convert(ros_any_command, other_proto_any_command) - assert other_proto_any_command.WhichOneof("commands") == "sit" - - -def test_messages_with_map_field() -> None: - proto_diagnostic = test_pb2.Diagnostic() - proto_diagnostic.severity = test_pb2.Diagnostic.Severity.FATAL - proto_diagnostic.reason = "Localization estimate diverged, cannot recover" - proto_diagnostic.attributes["origin"] = "localization subsystem" - - ros_diagnostic = proto2ros_tests.msg.Diagnostic() - convert(proto_diagnostic, ros_diagnostic) - assert ros_diagnostic.severity.value == proto_diagnostic.severity - assert ros_diagnostic.reason == proto_diagnostic.reason - assert len(ros_diagnostic.attributes) == 1 - assert ros_diagnostic.attributes[0].key == "origin" - assert ros_diagnostic.attributes[0].value == "localization subsystem" - - other_proto_diagnostic = test_pb2.Diagnostic() - convert(ros_diagnostic, other_proto_diagnostic) - assert other_proto_diagnostic.severity == proto_diagnostic.severity - assert other_proto_diagnostic.reason == proto_diagnostic.reason - assert other_proto_diagnostic.attributes == proto_diagnostic.attributes - - -def test_messages_with_deprecated_fields() -> None: - proto_remote_execution_request = test_pb2.RemoteExecutionRequest() - proto_remote_execution_request.prompt = "grab bike seat" - proto_remote_execution_request.timeout = 2 - proto_remote_execution_request.timeout_sec = 10.0 - - ros_remote_execution_request = proto2ros_tests.msg.RemoteExecutionRequest() - convert(proto_remote_execution_request, ros_remote_execution_request) - assert ros_remote_execution_request.prompt == proto_remote_execution_request.prompt - assert ros_remote_execution_request.timeout == proto_remote_execution_request.timeout - assert ros_remote_execution_request.timeout_sec == proto_remote_execution_request.timeout_sec - - other_proto_remote_execution_request = test_pb2.RemoteExecutionRequest() - convert(ros_remote_execution_request, other_proto_remote_execution_request) - assert other_proto_remote_execution_request.prompt == proto_remote_execution_request.prompt - assert other_proto_remote_execution_request.timeout == proto_remote_execution_request.timeout - assert other_proto_remote_execution_request.timeout_sec == proto_remote_execution_request.timeout_sec - - -def test_redefined_messages() -> None: - proto_remote_execution_result = test_pb2.RemoteExecutionResult() - proto_remote_execution_result.ok = False - proto_remote_execution_result.error.code = 255 - proto_remote_execution_result.error.reason = "interrupted" - proto_remote_execution_result.error.traceback.append("") - - ros_remote_execution_result = proto2ros_tests.msg.RemoteExecutionResult() - convert(proto_remote_execution_result, ros_remote_execution_result) - assert ros_remote_execution_result.ok == proto_remote_execution_result.ok - assert ros_remote_execution_result.error.code == proto_remote_execution_result.error.code - assert ros_remote_execution_result.error.reason == proto_remote_execution_result.error.reason - assert len(ros_remote_execution_result.error.traceback) > 0 - assert ros_remote_execution_result.error.traceback[0] == proto_remote_execution_result.error.traceback[0] - - other_proto_remote_execution_result = test_pb2.RemoteExecutionResult() - convert(ros_remote_execution_result, other_proto_remote_execution_result) - assert other_proto_remote_execution_result.ok == proto_remote_execution_result.ok - assert other_proto_remote_execution_result.error.code == proto_remote_execution_result.error.code - assert other_proto_remote_execution_result.error.reason == proto_remote_execution_result.error.reason - assert other_proto_remote_execution_result.error.traceback == proto_remote_execution_result.error.traceback - - -def test_messages_with_reserved_fields() -> None: - proto_displacement = test_pb2.Displacement() - proto_displacement.translation.x = 1.0 - proto_displacement.translation.y = 2.0 - proto_displacement.translation.z = 3.0 - - ros_displacement = proto2ros_tests.msg.Displacement() - convert(proto_displacement, ros_displacement) - assert ros_displacement.translation.x == proto_displacement.translation.x - assert ros_displacement.translation.y == proto_displacement.translation.y - assert ros_displacement.translation.z == proto_displacement.translation.z - assert not hasattr(ros_displacement, "rotation") - - other_proto_displacement = test_pb2.Displacement() - convert(ros_displacement, other_proto_displacement) - assert other_proto_displacement.translation.x == proto_displacement.translation.x - assert other_proto_displacement.translation.y == proto_displacement.translation.y - assert other_proto_displacement.translation.z == proto_displacement.translation.z - - -def test_message_forwarding() -> None: - proto_camera_info = test_pb2.CameraInfo() - proto_camera_info.height = 720 - proto_camera_info.width = 1280 - proto_camera_info.K.rows = 3 - proto_camera_info.K.cols = 3 - proto_camera_info.K.data[:] = [2000, 0, 800, 0, 2000, 800, 0, 0, 1] - proto_camera_info.R.rows = 3 - proto_camera_info.R.cols = 3 - proto_camera_info.R.data[:] = [1, 0, 0, 0, 1, 0, 0, 0, 1] - proto_camera_info.P.rows = 3 - proto_camera_info.P.cols = 4 - proto_camera_info.P.data[:] = [2000, 0, 800, 0, 0, 2000, 800, 0, 0, 0, 0, 1] - proto_camera_info.distortion_model.type = test_pb2.CameraInfo.DistortionModel.Type.PLUMB_BOB - proto_camera_info.distortion_model.coefficients[:] = [-0.2, -0.4, -0.0001, -0.0001, 0] - - ros_camera_info = proto2ros_tests.msg.CameraInfo() - convert(proto_camera_info, ros_camera_info) - assert ros_camera_info.height == proto_camera_info.height - assert ros_camera_info.width == proto_camera_info.width - assert ros_camera_info.k.rows == proto_camera_info.K.rows - assert ros_camera_info.k.cols == proto_camera_info.K.cols - assert list(ros_camera_info.k.data) == proto_camera_info.K.data - assert ros_camera_info.r.rows == proto_camera_info.R.rows - assert ros_camera_info.r.cols == proto_camera_info.R.cols - assert list(ros_camera_info.r.data) == proto_camera_info.R.data - assert ros_camera_info.p.rows == proto_camera_info.P.rows - assert ros_camera_info.p.cols == proto_camera_info.P.cols - assert list(ros_camera_info.p.data) == proto_camera_info.P.data - assert ros_camera_info.distortion_model.type.value == proto_camera_info.distortion_model.type - assert list(ros_camera_info.distortion_model.coefficients) == proto_camera_info.distortion_model.coefficients - - other_proto_camera_info = test_pb2.CameraInfo() - convert(ros_camera_info, other_proto_camera_info) - assert other_proto_camera_info.height == proto_camera_info.height - assert other_proto_camera_info.width == proto_camera_info.width - assert other_proto_camera_info.K.rows == proto_camera_info.K.rows - assert other_proto_camera_info.K.cols == proto_camera_info.K.cols - assert other_proto_camera_info.K.data == proto_camera_info.K.data - assert other_proto_camera_info.R.rows == proto_camera_info.R.rows - assert other_proto_camera_info.R.cols == proto_camera_info.R.cols - assert other_proto_camera_info.R.data == proto_camera_info.R.data - assert other_proto_camera_info.P.rows == proto_camera_info.P.rows - assert other_proto_camera_info.P.cols == proto_camera_info.P.cols - assert other_proto_camera_info.P.data == proto_camera_info.P.data - assert other_proto_camera_info.distortion_model.type == proto_camera_info.distortion_model.type - assert other_proto_camera_info.distortion_model.coefficients == proto_camera_info.distortion_model.coefficients - - -def test_messages_with_submessage_map_field() -> None: - proto_map = test_pb2.Map() - proto_fragment = proto_map.submaps[13] - proto_fragment.height = 20 - proto_fragment.width = 20 - proto_fragment.grid = b"\x00" * 20 * 20 - - ros_map = proto2ros_tests.msg.Map() - convert(proto_map, ros_map) - assert len(ros_map.submaps) == 1 - assert ros_map.submaps[0].key == 13 - ros_fragment = ros_map.submaps[0].value - assert ros_fragment.height == proto_fragment.height - assert ros_fragment.width == proto_fragment.width - assert ros_fragment.grid.tobytes() == proto_fragment.grid - - other_proto_map = test_pb2.Map() - convert(ros_map, other_proto_map) - assert len(other_proto_map.submaps) - other_proto_fragment = other_proto_map.submaps[13] - assert other_proto_fragment.height == proto_fragment.height - assert other_proto_fragment.width == proto_fragment.width - assert other_proto_fragment.grid == proto_fragment.grid - - -def test_messages_with_any_fields() -> None: - proto_matrix = test_pb2.Matrix() - proto_matrix.rows = 1 - proto_matrix.cols = 1 - proto_matrix.data.append(0) - - proto_request = test_pb2.RTTIQueryRequest() - proto_request.msg.Pack(proto_matrix) - - ros_request = proto2ros_tests.msg.RTTIQueryRequest() - convert(proto_request, ros_request) - assert isinstance(ros_request.msg, proto2ros.msg.AnyProto) - - unpacked_proto_matrix = test_pb2.Matrix() - convert(ros_request.msg, unpacked_proto_matrix) - assert unpacked_proto_matrix.rows == proto_matrix.rows - assert unpacked_proto_matrix.cols == proto_matrix.cols - assert unpacked_proto_matrix.data == proto_matrix.data - - other_proto_request = test_pb2.RTTIQueryRequest() - convert(ros_request, other_proto_request) - - other_unpacked_proto_matrix = test_pb2.Matrix() - other_proto_request.msg.Unpack(other_unpacked_proto_matrix) - assert other_unpacked_proto_matrix.rows == proto_matrix.rows - assert other_unpacked_proto_matrix.cols == proto_matrix.cols - assert other_unpacked_proto_matrix.data == proto_matrix.data - - -def test_messages_with_unknown_type_fields() -> None: - proto_result = test_pb2.RTTIQueryResult() - proto_result.type.name = "SomeMessage" - - ros_result = proto2ros_tests.msg.RTTIQueryResult() - convert(proto_result, ros_result) - assert isinstance(ros_result.type, proto2ros.msg.AnyProto) - - unpacked_proto_type = google.protobuf.type_pb2.Type() - convert(ros_result.type, unpacked_proto_type) - assert unpacked_proto_type.name == proto_result.type.name - - other_proto_result = test_pb2.RTTIQueryResult() - convert(ros_result, other_proto_result) - assert other_proto_result.type.name == proto_result.type.name - - -def test_messages_with_expanded_any_fields() -> None: - proto_target = bosdyn.api.geometry_pb2.SE2Pose() - proto_target.position.x = 1.0 - proto_target.position.y = -1.0 - proto_target.angle = math.pi - - proto_roi = bosdyn.api.geometry_pb2.Polygon() - for dx in (-0.5, 0.5): - for dy in (-0.5, 0.5): - vertex = proto_roi.vertexes.add() - vertex.x = proto_target.position.x + dx - vertex.y = proto_target.position.y + dy - - proto_goal = test_pb2.Goal() - proto_goal.target.Pack(proto_target) - proto_goal.roi.Pack(proto_roi) - - ros_goal = proto2ros_tests.msg.Goal() - convert(proto_goal, ros_goal) - assert isinstance(ros_goal.target, geometry_msgs.msg.Pose) - assert isinstance(ros_goal.roi, proto2ros.msg.Any) - assert ros_goal.target.position.x == proto_target.position.x - assert ros_goal.target.position.y == proto_target.position.y - assert ros_goal.roi.type_name == "geometry_msgs/Polygon" - - other_proto_goal = test_pb2.Goal() - convert(ros_goal, other_proto_goal) - other_proto_target = bosdyn.api.geometry_pb2.SE2Pose() - other_proto_goal.target.Unpack(other_proto_target) - assert proto_target.position.x == other_proto_target.position.x - assert proto_target.position.y == other_proto_target.position.y - assert proto_target.angle == other_proto_target.angle - - other_proto_roi = bosdyn.api.geometry_pb2.Polygon() - other_proto_goal.roi.Unpack(other_proto_roi) - assert len(other_proto_roi.vertexes) == 4 - for a, b in zip(proto_roi.vertexes, other_proto_roi.vertexes, strict=True): - assert a.x == b.x and a.y == b.y diff --git a/synchros2/README.md b/synchros2/README.md new file mode 100644 index 0000000..0aad879 --- /dev/null +++ b/synchros2/README.md @@ -0,0 +1,961 @@ +# `synchros2` + +At its core, `synchros2` is nothing but a collection of utilities and wrappers built on top of [`rclpy`](https://github.com/ros2/rclpy). When used in concert, these utilities and wrappers simplify ROS 2 usage by enabling standard, idiomatic, synchronous Python programming. To that end, `synchros2` relies on heavy yet implicit concurrency and thus there is overhead in its simplicity. + +## Table of contents + +- [Features](#features) + - [Process-wide APIs](#process-wide-apis) + - [Actionable and serviced APIs](#actionable-and-serviced-apis) + - [Message feed APIs](#message-feed-apis) +- [Guidelines](#guidelines) + - [Integration testing](#integration-testing) + +## Features + +### Process-wide APIs + +Process-wide APIs are built around the notion of a ROS 2 aware _scope_. ROS 2 aware scopes manage the lifetime of a +thread local graph of ROS 2 nodes, along with an executor that dispatches work for them. ROS 2 nodes may be +loaded and unloaded (i.e. instantiated and put to spin, and explicitly destroyed, respectively) or have their +entire lifecycle be managed (i.e. bound to a context manager). A ROS 2 aware scope may also define a main +ROS 2 node (for ease of use, for log forwarding, etc.). ROS 2 aware scopes may be nested, enforcing locality +of ROS 2 usage in a codebase, though the innermost scope is always accessible through `synchros2.scope` +module-level APIs. + +A ROS 2 aware scope may also be process local (i.e. global within a process), which allows for the notion +of a ROS 2 aware _process_. Only one ROS 2 aware process may be active at any point in time as a top-level +scope i.e. a ROS 2 aware process may not start within an existing ROS 2 aware scope. The current ROS 2 aware +process and associated scope are always accessible process-wide through `synchros2.process` +module-level APIs. + +These notions afford process-wide (and thread-wide) access to locally managed ROS 2 entities and thus ownership +and lifetime is well defined. Moreover, callbacks are dispatched in the background by default, enabling both +synchronous and asynchronous programming out-of-the-box. + +These APIs are also quite handy to reconcile past [`rospy`](http://wiki.ros.org/rospy) experience with ROS 2. + +#### Common use cases + +##### Setting up single node processes + +To make use of ROS 2 without getting into the details, just decorate your executable entrypoint (or `main` function) +with `synchros2.process.main`: + +```python +import logging +import time + +import synchros2.process as ros_process + +import std_msgs.msg + +@ros_process.main() +def entrypoint() -> None: + # no need to initialize, it is automatic + node = ros_process.node() # or entrypoint.node + assert node is not None + pub = node.create_publisher(std_msgs.msg.String, "test", 1) + + def callback() -> None: + time.sleep(10) # you can block in a callback + pub.publish(std_msgs.msg.String(data="testing")) + executor = ros_process.executor() # or entrypoint.executor + assert executor is not None + executor.create_task(callback) # dispatch callback for execution + + time.sleep(10) # you can block in the main thread + + node.get_logger().info("testing") + logging.info("testing") # you can use Python logging + + try: + ros_process.wait_for_shutdown() # you can wait for Ctrl + C + except KeyboardInterrupt: + pass # to avoid traceback printing + + return # no need to cleanup or shutdown, it is automatic + +if __name__ == "__main__": + entrypoint() +``` + +Note a ROS 2 node and an executor are accessible process-wide through `synchros2.process` module APIs. +This is ideal for quick prototyping and simple scripts, as the UX is largely intuitive e.g. you can make blocking +calls from virtually anywhere. + +##### Setting up multi-node processes + +You can spin as many ROS 2 nodes as you need, and skip the default process-wide node if unnecessary. Here's an example that +loads three (3) ROS 2 nodes and spins them indefinitely: + +```python +import logging +import time + +from typing import Any, List + +import synchros2.process as ros_process +from synchros2.node import Node + +def graph(**kwargs: Any) -> List[Node]: + # make sure to forward all keyword arguments for proper loading! + return [Node("my_node", **kwargs), Node("my_other_node", **kwargs)] + +@ros_process.main(prebaked=False) +def main() -> None: + ros_process.load(Node, "_hidden_node") # or main.load + ros_process.spin(graph) # or main.spin, and it will block! + +if __name__ == "__main__": + main() +``` + +Note the use of ROS 2 node(s) factories, to load (or spin) entire collections at once. + +##### Setting up interactive multi-node applications + +For interactive applications, you will most likely want to keep automatic spinning in place. Here's an example that waits +for user input, sleeps for 5 seconds, and then echoes that user input from a callback without deadlocking: + +```python +import logging +import time + +from typing import Any, List + +from synchros2.futures import wait_for_future +from synchros2.node import Node +import synchros2.process as ros_process + +def graph(**kwargs: Any) -> List[Node]: + # make sure to forward all keyword arguments for proper loading! + return [Node("my_node", **kwargs), Node("my_other_node", **kwargs)] + +def work(prompt: str) -> None: + def worker() -> None: + node = ros_process.node() + assert node is not None + time.sleep(5.0) # sleeping is safe! + node.get_logger().info(prompt + " done!") + executor = ros_process.executor() + assert executor is not None + future = executor.create_task(worker) # dispatch worker to executor + wait_for_future(future) # block until worker is done + +@ros_process.main() +def main() -> None: + ros_process.load(graph) # or main.load + while True: + work(input()) + +if __name__ == "__main__": + main() +``` + +Note the use of `input` to read from `stdin`. This effectively prevents this executable from running with `launch`, +as `launch` does **not** pipe its own process `stdin` to that of any executed subprocess. + +##### Setting up command-line single node applications + +Command-line arguments may affect ROS 2 configuration if need be: + +```python +import argparse +import logging + +import synchros2.process as ros_process +import synchros2.scope as ros_scope + +class Application: + + def __init__(self, robot_name: str) -> None: + self.robot_name = robot_name + self.node = ros_scope.node() + +def cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("robot_name") + parser.add_argument("-v", "--verbose", action="store_true") + # define ROS 2 aware process arguments based on CLI arguments + parser.set_defaults(process_args=lambda args: dict(forward_logging=args.verbose)) + return parser + +@ros_process.main(cli(), autospin=False) +def main(args: argparse.Namespace) -> None: + app = Application(args.robot_name) + if args.verbose: + root = logging.getLogger() + root.setLevel(logging.INFO) + logging.info("Application started!") + ros_process.spin() # or main.spin + +if __name__ == "__main__": + main() +``` + +Note that process arguments are set via CLI. Also, note that a process-wide node is set explicitly (rather than implicitly). +The same can be done for process-wide executors. This allows fine grained control over ROS 2 configuration, which is useful +to those with special needs e.g. a custom main node (like in this case), a custom executor, etc. + +##### Writing ROS 2 aware libraries + +You can use process-wide APIs to fetch ROS 2 defaults deep down call hierarchies, when passing nodes explicitly adds unnecessary clutter: + +```python +import time + +import synchros2.process as ros_process +import synchros2.scope as ros_scope +from synchros2.node import Node + +class SomeFeature: + + def __init__(self) -> None: + self.node = ros_scope.load(Node, "my_node") + + def shutdown(self) -> None: + ros_scope.unload(self.node) # or leave it behind until process ends + +@ros_process.main() +def main() -> None: + feature = SomeFeature() + try: + time.sleep(10.0) + finally: + feature.shutdown() + +if __name__ == "__main__": + main() +``` + +Note the use of `ros_scope` instead of `ros_process`. Both offer roughly the same APIs, but the former reaches out to the innermost scope, +whereas the latter presumes a process is active. Therefore, and as a rule of thumb, libraries should always use `synchros2.scope` APIs. +This will allow such libraries to work in more complex, multi-threaded, multi-scope applications, and simplify testing. + +##### Writing ROS 2 aware tests + +To write tests, set up a global ROS 2 aware scope instead of a process. Also, consider using namespaces to avoid default, hidden ROS 2 names. + +Using [`pytest`](https://docs.pytest.org/en/6.2.x/contents.html) (recommended): + +```python +from typing import Iterator + +import synchros2.scope as ros_scope +from synchros2.scope import ROSAwareScope + +import pytest + +@pytest.fixture +def ros() -> Iterator[ROSAwareScope]: + """ + A pytest fixture that will set up and yield a ROS 2 aware global scope to each test that requests it. + + See https://docs.pytest.org/en/6.2.x/fixture.html for a primer on pytest fixtures. + """ + with ros_scope.top(global_=True, namespace="fixture") as top: + yield top + +def test_it(ros: ROSAwareScope) -> None: + assert ros.node is not None + ros.node.get_logger().info("Logging!") +``` + +Using [`unittest`](https://docs.python.org/3/library/unittest.html): + +```python +import unittest + +import synchros2.scope as ros_scope + +class TestCase(unittest.TestCase): + + def test_it(self) -> None: + with ros_scope.top(global_=True, namespace="fixture") as ros: + self.assertIsNotNone(ros.node) + ros.node.get_logger().info("Logging!") +``` + +### Actionable and serviced APIs + +These APIs wrap those in [`rclpy.client`](https://github.com/ros2/rclpy/tree/rolling/rclpy/rclpy/client.py) and [`rclpy.action.client`](https://github.com/ros2/rclpy/tree/rolling/rclpy/rclpy/action/client.py) to provide a simpler UX when dealing with ROS 2 actions and services. + +Actionable and serviced APIs abstract ROS 2 action and service calls behind an interface that resembles that of remote procedure calls. These can be invoked either synchronously or asynchronously. When used asynchronously, serviced APIs return plain futures whereas actionable APIs return action futures. Action futures build on the notion of a future to track actions' feedback, status, and result. + +Both abstractions are well integrated with [ROS 2 aware scopes and processes](#process-wide-apis). + +#### Common use cases + +The following snippets make use of standard ROS 2 [`examples`](https://index.ros.org/r/examples/github-ros2-examples) and [`example_interfaces`](https://index.ros.org/p/example_interfaces). + +##### Invoking a service synchronously + +You can use a serviced API as you would use any other callable: + +```python +import argparse + +from example_interfaces.srv import AddTwoInts + +from synchros2.service import Serviced, ServiceTimeout, ServiceError +import synchros2.process as ros_process + +def cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("a", type=int) + parser.add_argument("b", type=int) + return parser + +@ros_process.main(cli()) +def main(args: argparse.Namespace) -> None: + add_two_ints = Serviced(AddTwoInts, "add_two_ints") + if not add_two_ints.wait_for_service(timeout_sec=5.0): + print(f"No {add_two_ints.service_name} services found") + return + try: + print(f"Computing {args.a} + {args.b}...") + result = add_two_ints(AddTwoInts.Request(a=args.a, b=args.b), timeout_sec=5.0) + print("Result is", result.sum) + except ServiceTimeout: + print("Computation timed out") + except ServiceError as e: + print(f"Computation failed: {e}") + +if __name__ == "__main__": + main() +``` + +**Note**: you may use servers in the `examples_rclpy_minimal_service` package to test this. + +Serviced API calls are synchronous by default. This can also be made explicit by calling `synchronously()` on them instead e.g. `add_two_ints.synchronously()`. +All service outcomes other than nominal success are signaled using exceptions. An optional timeout prevents calling (and blocking on) a service request indefinitely. + +##### Invoking a service asynchronously + +You can get a future service response instead of blocking on call too: + +```python +import argparse + +from example_interfaces.srv import AddTwoInts + +from synchros2.service import Serviced +from synchros2.futures import wait_for_future +import synchros2.process as ros_process + +def cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("a", type=int) + parser.add_argument("b", type=int) + return parser + +@ros_process.main(cli()) +def main(args: argparse.Namespace) -> None: + add_two_ints = Serviced(AddTwoInts, "add_two_ints") + if not add_two_ints.wait_for_service(timeout_sec=5.0): + print(f"No {add_two_ints.service_name} services found") + return + print(f"Computing {args.a} + {args.b}...") + future = add_two_ints.asynchronously(AddTwoInts.Request(a=args.a, b=args.b)) + if not wait_for_future(future, timeout_sec=5.0): + print("Computation did not complete in time") + future.cancel() + return + result = future.result() + print("Result is", result.sum) + +if __name__ == "__main__": + main() +``` + +**Note**: you may use servers in the `examples_rclpy_minimal_service` package to test this. + +Service response must be waited on, either explicitly and with a timeout or implicitly by early result request. +Note fetching the future call result may raise. + +##### Invoking an action synchronously + +You can use an actionable API as you would use any other callable: + +```python +import argparse + +from example_interfaces.action import Fibonacci + +from synchros2.action import Actionable +from synchros2.action import ( + ActionTimeout, ActionRejected, ActionCancelled, ActionAborted +) +import synchros2.process as ros_process + +def cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("order", type=int) + return parser + +@ros_process.main(cli()) +def main(args: argparse.Namespace) -> None: + compute_fibonacci_sequence = Actionable(Fibonacci, "fibonacci") + if not compute_fibonacci_sequence.wait_for_server(timeout_sec=5.0): + print(f"No {compute_fibonacci_sequence.action_name} action server found") + return + try: + print(f"Computing Fibonacci sequence for order N = {args.order}...") + result = compute_fibonacci_sequence(Fibonacci.Goal(order=args.order), timeout_sec=5.0) + print("Sequence is", result.sequence) + except ActionRejected: + print("Computation rejected") + except ActionTimeout: + print("Computation timed out") + except ActionAborted: + print("Computation aborted") + except ActionCancelled: + print("Computation cancelled") + +if __name__ == "__main__": + main() +``` + +**Note**: you may use servers in the `examples_rclpy_minimal_action_server` package to test this. + +Actionable API calls are synchronous by default. This can also be made explicit by calling `synchronously()` on them instead e.g. `compute_fibonacci_sequence.synchronously()`. +All action outcomes other than nominal success are signaled using exceptions. Action feedback is ignored unless a callback is specified on call. +An optional timeout prevents pursuing (and blocking on) an action indefinitely. + +##### Invoking an action asynchronously + +You can get a future to an ongoing action instead of blocking on it: + +```python +import argparse + +from example_interfaces.action import Fibonacci + +from synchros2.action import Actionable +from synchros2.futures import wait_for_future +import synchros2.process as ros_process + +def cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("order", type=int) + return parser + +@ros_process.main(cli()) +def main(args: argparse.Namespace) -> None: + compute_fibonacci_sequence = Actionable(Fibonacci, "fibonacci") + if not compute_fibonacci_sequence.wait_for_server(timeout_sec=5.0): + print(f"No {compute_fibonacci_sequence.action_name} action server found") + return + print(f"Computing Fibonacci sequence for order N = {args.order}...") + action = compute_fibonacci_sequence.asynchronously( + Fibonacci.Goal(order=args.order), track_feedback=True + ) + wait_for_future(action.acknowledgement, timeout_sec=5.0) + if not action.acknowledged or not action.accepted: + print("Computation rejected") + return + for feedback in action.feedback_stream(timeout_sec=5.0): + print(f"Partial sequence is", feedback.sequence) + if not wait_for_future(action.finalization, timeout_sec=5.0): + print("Computation did not complete in time") + action.cancel() + return + if action.succeeded: + print("Sequence is", action.result.sequence) + elif action.aborted: + print("Computation aborted") + elif action.cancelled: + print("Computation cancelled") + else: + print("Internal server error") + +if __name__ == "__main__": + main() +``` + +**Note**: you may use servers in the `examples_rclpy_minimal_action_server` package to test this. + +Action status must be checked explicitly, and timely before attempting to access an action's result or feedback (which may not be there yet). +Action acknowledgement and finalization futures can help synchronization. Action feedback streaming simplifies (soft) real-time action monitoring. + +### Message feed APIs + +These APIs wrap [`message_filters`](https://index.ros.org/r/message_filters) to provide a simpler UX when dealing with streams of messages, including but not limited to ROS 2 topic subscriptions. + +Message feeds are the stateful generalization of standard ROS 2 message filters. Any message filter can become a feed, allowing: + +- latest message retrieval +- message history retrieval +- message callback registration (and de-registration) +- message stream iteration as they arrive, one-by-one or in batches +- incoming message waits -- any message or those matching a predicate + +Like message filters, most message feeds can be chained. This is true for all but those that externally source messages, ROS 2 topic subscriptions being the prime example of this. Other message feeds built into `synchros2` offer a vehicle for generic map-filter-reduce patterns, time synchronization across multiple message feeds, and synchronized `tf` lookups. + +On word of caution. While any message filter can become a feed, standard ROS 2 message filters are usually not thread-safe. See [`synchros2.filters`](https://github.com/bdaiinstitute/ros_utilities/tree/main/synchros2/synchros2/filters.py) for thread-safe (re)implementations. + +#### Common use cases + +The following snippets make use of standard ROS 2 [`examples`](https://index.ros.org/r/examples/github-ros2-examples). +You may use the publishers in the `examples_rclpy_minimal_publisher` package to test these. + +##### Looping over topic messages + +```python +from contextlib import closing +from std_msgs.msg import String + +from synchros2.subscription import Subscription +import synchros2.process as ros_process + +@ros_process.main() +def main() -> None: + topic_data = Subscription(String, "topic") + with closing(topic_data.stream()) as stream: + for message in stream: + print(message.data) + +if __name__ == "__main__": + main() +``` + +Note that the topic message stream is managed and closed explicitly by the [`contextlib.closing`](https://docs.python.org/3/library/contextlib.html#contextlib.closing) context manager. This is important to stop message buffering as soon as it is no longer necessary. This can be deferred to the [garbage collector](https://docs.python.org/3/library/gc.html) but it may result in needless buffering for (much) longer. + +##### Waiting for the next topic message + +```python +from std_msgs.msg import String + +from synchros2.subscription import Subscription +from synchros2.futures import unwrap_future +import synchros2.process as ros_process + +@ros_process.main() +def main() -> None: + topic_data = Subscription(String, "topic") + while main.context.ok(): + message = unwrap_future(topic_data.update, timeout_sec=5.0) + print(message.data) + +if __name__ == "__main__": + main() +``` + +Note that the future update is _unwrapped_ rather than waited on. If the future update does not become available in time, future unwrapping will raise. + +##### Waiting for a specific topic message + +```python +from std_msgs.msg import Int32 +from std_msgs.msg import String + +from synchros2.subscription import Subscription +from synchros2.futures import unwrap_future +import synchros2.process as ros_process + +def to_int32(message: String) -> Int32: + return Int32(data=int(message.data.rpartition(" ")[-1])) + +@ros_process.main() +def main() -> None: + topic_data = Subscription(String, "topic") + while main.context.ok(): + message = unwrap_future(topic_data.matching_update( + lambda message: to_int32(message).data % 5 == 0 + ), timeout_sec=5.0) + print(message.data) + +if __name__ == "__main__": + main() +``` + +##### Setting up a message callback + +```python +from std_msgs.msg import String + +from synchros2.subscription import Subscription +import synchros2.process as ros_process + +@ros_process.main() +def main() -> None: + topic_data = Subscription(String, "topic") + topic_data.recall(lambda message: print(message.data)) + main.wait_for_shutdown() + +if __name__ == "__main__": + main() +``` + +##### Synchronizing topic messages + +```python +from contextlib import closing +from std_msgs.msg import String + +from synchros2.feeds import SynchronizedMessageFeed +from synchros2.futures import unwrap_future +from synchros2.subscription import Subscription +import synchros2.process as ros_process + +@ros_process.main() +def main() -> None: + topics_data = SynchronizedMessageFeed( + Subscription(String, "topic0"), + Subscription(String, "topic1"), + allow_headerless=True + ) + with closing(topics_data.stream()) as stream: + for a, b in stream: + print("a:", a.data, "matches b:", b.data) + +if __name__ == "__main__": + main() +``` + +##### Adapting topic messages + +```python +from contextlib import closing +from typing import Optional +from std_msgs.msg import String +from std_msgs.msg import Int32 + +from synchros2.feeds import AdaptedMessageFeed +from synchros2.subscription import Subscription +import synchros2.process as ros_process + +def to_int32(message: String) -> Int32: + return Int32(data=int(message.data.rpartition(" ")[-1])) + +def keep_even(message: Int32) -> Optional[Int32]: + return message if message.data % 2 == 0 else None + +@ros_process.main() +def main() -> None: + topic_data = AdaptedMessageFeed( + Subscription(String, "topic"), + lambda msg: keep_even(to_int32(msg)) + ) + with closing(topic_data.stream()) as stream: + for message in stream: + print(message.data) + +if __name__ == "__main__": + main() +``` + +Note that the adapter logic not only transforms the message type but also filters them. Returning `None` stops message propagation down the chain of message feeds. + +##### Fetch the last 10 topic messages + +```python +from contextlib import closing + +from std_msgs.msg import String + +from synchros2.subscription import Subscription +from synchros2.time import Duration +import synchros2.process as ros_process + +@ros_process.main() +def main() -> None: + clock = main.node.get_clock() + with closing(Subscription(String, "topic", history_length=10)) as topic_data: + print("Waiting for 5 seconds...") + clock.sleep_until(clock.now() + Duration(seconds=5.0)) + for message in topic_data.history: + print(message.data) + +if __name__ == "__main__": + main() +``` + +### Logging interoperability + +To facilitate logging, `synchros2` include functionality to bridge code using Python standard [`logging`](https://docs.python.org/3/library/logging.html) with the ROS 2 logging system. + +#### API review + +##### Standard `logging` + +```python +import logging + +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logger.info("Logged using standard logging") +``` + +These logs will be handled by Python's standard `logging` module (i.e. printed to console). + +##### Standalone `rclpy` logging + +```python +import rclpy.logging + +logger = rclpy.logging.get_logger(__name__) +logger.set_level(rclpy.logging.LoggingSeverity.INFO) +logger.info("Logged using a standalone rclpy logger") +``` + +These logs will be ultimately handled by `rcutils` logging system (i.e. printed to console, perhaps forwarded to `spdlog`). + +###### `rclpy` node logging + +```python +import rclpy +import rclpy.logging + +node = rclpy.create_node("my_node") +logger = node.get_logger() +logger.set_level(rclpy.logging.LoggingSeverity.INFO) +logger.info("Logged using an rclpy node logger") +``` + +These logs will be ultimately handled by `rcutils` logging system too, but will also be published to `/rosout`. + +#### Log bridging + +Managing multiple, independent logging systems is impractical. So is reworking a codebase to accommodate either. To avoid both scenarios, use `synchros2.logging.logs_to_ros` explicitly: + +```python +import logging +import rclpy + +from synchros2.logging import logs_to_ros + +node = rclpy.create_node("my_node") +with logs_to_ros(node): + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + logger.info("Logged using standard logging") +``` + +or implicitly through [process-wide APIs](#process-wide-apis): + +```bash +import logging + +from synchros2.process as ros_process + +@ros_process.main() +def main(): + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + logger.info("Logged using standard logging") + +if __name__ == "__main__": + main() +``` + +`logging` logs will propagate up the logger hierarchy and then forwarded to the logger of the corresponding node (the one provided in the first case, or the main prebaked node in the second case) and thus at least printed to console and published to `/rosout`. Note the above presumes that severity levels at every step of the way are such that the corresponding log will get through (in this example, INFO or below for both `logging` and ROS 2 logging system). + +## Guidelines + +### Integration testing + +Unit testing ROS 2 code is no different from unit testing any other software, but some care must be exercised. + +#### Considerations for integration testing + +* Data transport in ROS 2 is non-deterministic. So is callback execution when multi-threaded executors are in place. This is true for ROS 2 code in general, and for [process-wide APIs](#process-wide-apis) in particular, for which non-determinism is the price to pay for a synchronous programming model. As such, time sensitive and execution order dependent tests are bound to fail, even if only sporadically. Synchronization is necessary to avoid these issues, and fortunately the very same process-wide APIs enable safe use of synchronization primitives (e.g. via a multi-threaded executor spinning in a background thread, as provided by [`bdai_ros_wrappers.scope`](https://github.com/bdaiinstitute/ros_utilities/blob/main/synchros2/synchros2/scope.py) functionality). +* ROS 2 middlewares perform peer discovery by default. This allows distributed architectures in production but leads to cross-talk during parallelized testing. [`domain_coordinator`](https://github.com/ros2/ament_cmake_ros/tree/rolling/domain_coordinator) functionality simplifies [ROS domain ID](https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Domain-ID.html) assignment enforcing host-wide uniqueness and with it, middleware isolation. + +Therefore, as rules of thumb consider: + +1. Using `synchros2.scope.top` to setup ROS 2 in your test fixtures. + * Isolate it by passing a unique domain ID, as provided by `domain_coordinator.domain_id`. +1. Using synchronization primitives to wait with timeouts. + * Note timeouts make the test time sensitive. Pick timeouts an order of magnitude above the expected test timing. + +#### Writing integration tests using `pytest` (recommended) + +[`pytest`](https://docs.pytest.org/en/7.4.x/) is a testing framework for Python software, the most common in ROS 2 Python codebases. + +```python +import domain_coordinator +import pytest + +from typing import Iterator + +import synchros2.scope as ros_scope +from synchros2.action_client import ActionClientWrapper +from synchros2.futures import wait_for_future +from synchros2.scope import ROSAwareScope +from synchros2.single_goal_action_server import SingleGoalActionServer +from synchros2.subscription import wait_for_message + +from std_msgs.msg import String +from std_srvs.srv import Trigger +from example_interfaces.action import Fibonacci + +from rclpy.action.server import ServerGoalHandle +from rclpy.qos import QoSProfile, HistoryPolicy, DurabilityPolicy + +@pytest.fixture +def ros() -> Iterator[ROSAwareScope]: + """ + A pytest fixture that will set up and yield a ROS 2 aware global scope to each test that requests it. + + See https://docs.pytest.org/en/7.4.x/fixture.html for a primer on pytest fixtures. + """ + with domain_coordinator.domain_id() as domain_id: # to ensure node isolation + with ros_scope.top(global_=True, namespace="fixture", domain_id=domain_id) as top: + yield top + +def test_topic_pub_sub(ros: ROSAwareScope) -> None: + """Asserts that a published message can be received on the other end.""" + qos_profile = QoSProfile( + depth=100, + history=HistoryPolicy.KEEP_LAST, + durability=DurabilityPolicy.TRANSIENT_LOCAL, + ) + pub = ros.node.create_publisher(String, "test", qos_profile) + pub.publish(String(data="test")) + # Message will arrive at an unspecified point in the future, thus + assert wait_for_message(String, "test", qos_profile=qos_profile, timeout_sec=5.0) + +def test_service_server_client(ros: ROSAwareScope) -> None: + """Asserts that a service server replies to client requests.""" + def callback(_: Trigger.Request, response: Trigger.Response) -> Trigger.Response: + response.success = True + return response + server = ros.node.create_service(Trigger, "trigger", callback) + client = ros.node.create_client(Trigger, "trigger") + assert client.wait_for_service(timeout_sec=5.0) + future = client.call_async(Trigger.Request()) + assert wait_for_future(future, timeout_sec=5.0) + response = future.result() + assert response and response.success + +def test_action_server_client(ros: ROSAwareScope) -> None: + """Asserts that an action server reacts to action client requests.""" + def callback(goal_handle: ServerGoalHandle) -> Fibonacci.Result: + result = Fibonacci.Result() + result.sequence = [0, 1] + for i in range(1, goal_handle.request.order): + result.sequence.append(result.sequence[i] + result.sequence[i-1]) + goal_handle.succeed() + return result + server = SingleGoalActionServer(ros.node, Fibonacci, "compute", callback) + client = ActionClientWrapper(Fibonacci, "compute", ros.node) + assert client.wait_for_server(timeout_sec=5.0) + goal = Fibonacci.Goal(order=3) + result = client.send_goal_and_wait("compute", goal, timeout_sec=5.0) + assert result and list(result.sequence) == [0, 1, 1, 2] +``` + +#### Writing integration tests using `unittest` + +[`unittest`](https://docs.python.org/3/library/unittest.html) is the testing framework in Python's standard library. + +```python +import contextlib +import domain_coordinator +import unittest + +import synchros2.scope as ros_scope +from synchros2.action_client import ActionClientWrapper +from synchros2.futures import wait_for_future +from synchros2.scope import ROSAwareScope +from synchros2.single_goal_action_server import SingleGoalActionServer +from synchros2.subscription import wait_for_message + +from std_msgs.msg import String +from std_srvs.srv import Trigger +from example_interfaces.action import Fibonacci + +from rclpy.action.server import ServerGoalHandle +from rclpy.qos import QoSProfile, HistoryPolicy, DurabilityPolicy + +class TestCase(unittest.TestCase): + + def setUp(self) -> None: + """Sets up an isolated ROS 2 aware scope for all tests in the test case.""" + self.fixture = contextlib.ExitStack() + domain_id = self.fixture.enter_context(domain_coordinator.domain_id()) + self.ros = self.fixture.enter_context(ros_scope.top( + global_=True, namespace="fixture", domain_id=domain_id + )) + + def tearDown(self) -> None: + self.fixture.close() # exits all contexts + + def test_topic_pub_sub(self) -> None: + """Asserts that a published message can be received on the other end.""" + qos_profile = QoSProfile( + depth=100, + history=HistoryPolicy.KEEP_LAST, + durability=DurabilityPolicy.TRANSIENT_LOCAL, + ) + pub = self.ros.node.create_publisher(String, "test", qos_profile) + pub.publish(String(data="test")) + # Message will arrive at an unspecified point in the future, thus + self.assertIsNotNone(wait_for_message( + String, "test", qos_profile=qos_profile, timeout_sec=5.0)) + + def test_service_server_client(self) -> None: + """Asserts that a service server replies to client requests.""" + def callback(_: Trigger.Request, response: Trigger.Response) -> Trigger.Response: + response.success = True + return response + server = self.ros.node.create_service(Trigger, "trigger", callback) + client = self.ros.node.create_client(Trigger, "trigger") + self.assertTrue(client.wait_for_service(timeout_sec=5.0)) + future = client.call_async(Trigger.Request()) + self.assertTrue(wait_for_future(future, timeout_sec=5.0)) + response = future.result() + self.assertIsNotNone(response) + self.assertTrue(response.success) + + def test_action_server_client(self) -> None: + """Asserts that an action server reacts to action client requests.""" + def callback(goal_handle: ServerGoalHandle) -> Fibonacci.Result: + result = Fibonacci.Result() + result.sequence = [0, 1] + for i in range(1, goal_handle.request.order): + result.sequence.append(result.sequence[i] + result.sequence[i-1]) + goal_handle.succeed() + return result + server = SingleGoalActionServer(self.ros.node, Fibonacci, "compute", callback) + client = ActionClientWrapper(Fibonacci, "compute", self.ros.node) + self.assertTrue(client.wait_for_server(timeout_sec=5.0)) + goal = Fibonacci.Goal(order=3) + result = client.send_goal_and_wait("compute", goal, timeout_sec=5.0) + self.assertIsNotNone(result) + self.assertEqual(list(result.sequence), [0, 1, 1, 2]) +``` + +#### Adding integration tests to a package + +A package's type and build system dictate how unit tests are to be added. Unit tests for ROS 2 packages are typically hosted under the `test` subdirectory, so the following assumes this convention is observed. + +For `ament_cmake` packages, the `CMakeLists.txt` file should have: +```cmake +if(BUILD_TESTING) + find_package(ament_cmake_pytest REQUIRED) + # Define an arbitrary target for your tests such as: + ament_add_pytest_test(unit_tests test) +endif() +``` + +For `ament_python` packages, the `setup.py` file should have: +```python +setup( + # ... + tests_require=['pytest'], +) +``` + +Note that `pytest` is the testing tool of choice regardless of package type. + +**Note**: [test discovery mechanisms in `pytest`](https://docs.pytest.org/en/7.4.x/explanation/goodpractices.html#conventions-for-python-test-discovery) do not require `__init__.py` files under `test` directories. + +#### Useful references + +- [Official ROS 2 testing tutorial](https://docs.ros.org/en/rolling/Tutorials/Intermediate/Testing/Testing-Main.html) +- [`pytest` documentation](https://pytest.org) +- [`unittest` documentation](https://docs.python.org/3/library/unittest.html) diff --git a/bdai_ros2_wrappers/examples/background_action_example.py b/synchros2/examples/background_action_example.py similarity index 96% rename from bdai_ros2_wrappers/examples/background_action_example.py rename to synchros2/examples/background_action_example.py index 2801bc8..17b2526 100644 --- a/bdai_ros2_wrappers/examples/background_action_example.py +++ b/synchros2/examples/background_action_example.py @@ -18,8 +18,8 @@ from rclpy.action.client import ActionClient from rclpy.action.server import ActionServer, ServerGoalHandle -import bdai_ros2_wrappers.process as ros_process -from bdai_ros2_wrappers.node import Node +import synchros2.process as ros_process +from synchros2.node import Node class MinimalActionServer(Node): diff --git a/bdai_ros2_wrappers/examples/interruptible_talker_example.py b/synchros2/examples/interruptible_talker_example.py similarity index 95% rename from bdai_ros2_wrappers/examples/interruptible_talker_example.py rename to synchros2/examples/interruptible_talker_example.py index 5a445b6..a00dc0b 100644 --- a/bdai_ros2_wrappers/examples/interruptible_talker_example.py +++ b/synchros2/examples/interruptible_talker_example.py @@ -24,9 +24,9 @@ import std_msgs.msg from rclpy.executors import SingleThreadedExecutor -import bdai_ros2_wrappers.process as ros_process -from bdai_ros2_wrappers.executors import foreground -from bdai_ros2_wrappers.node import Node +import synchros2.process as ros_process +from synchros2.executors import foreground +from synchros2.node import Node class InterruptibleTalkerNode(Node): diff --git a/bdai_ros2_wrappers/examples/logs_to_ros_example.py b/synchros2/examples/logs_to_ros_example.py similarity index 93% rename from bdai_ros2_wrappers/examples/logs_to_ros_example.py rename to synchros2/examples/logs_to_ros_example.py index e236115..5928998 100644 --- a/bdai_ros2_wrappers/examples/logs_to_ros_example.py +++ b/synchros2/examples/logs_to_ros_example.py @@ -14,7 +14,7 @@ import rclpy -from bdai_ros2_wrappers.logging import logs_to_ros +from synchros2.logging import logs_to_ros def main() -> None: diff --git a/bdai_ros2_wrappers/examples/talker_example.py b/synchros2/examples/talker_example.py similarity index 95% rename from bdai_ros2_wrappers/examples/talker_example.py rename to synchros2/examples/talker_example.py index d6f6d69..e379c21 100644 --- a/bdai_ros2_wrappers/examples/talker_example.py +++ b/synchros2/examples/talker_example.py @@ -20,8 +20,8 @@ import std_msgs.msg -import bdai_ros2_wrappers.process as ros_process -from bdai_ros2_wrappers.node import Node +import synchros2.process as ros_process +from synchros2.node import Node class TalkerNode(Node): diff --git a/bdai_ros2_wrappers/examples/talker_listener_example.py b/synchros2/examples/talker_listener_example.py similarity index 96% rename from bdai_ros2_wrappers/examples/talker_listener_example.py rename to synchros2/examples/talker_listener_example.py index 73384cb..3a0c46a 100644 --- a/bdai_ros2_wrappers/examples/talker_listener_example.py +++ b/synchros2/examples/talker_listener_example.py @@ -14,8 +14,8 @@ import std_msgs.msg -import bdai_ros2_wrappers.process as ros_process -from bdai_ros2_wrappers.node import Node +import synchros2.process as ros_process +from synchros2.node import Node class TalkerNode(Node): diff --git a/bdai_ros2_wrappers/examples/tf_cli_example.py b/synchros2/examples/tf_cli_example.py similarity index 94% rename from bdai_ros2_wrappers/examples/tf_cli_example.py rename to synchros2/examples/tf_cli_example.py index 8bf4833..0c0437a 100644 --- a/bdai_ros2_wrappers/examples/tf_cli_example.py +++ b/synchros2/examples/tf_cli_example.py @@ -20,11 +20,11 @@ from rclpy.executors import SingleThreadedExecutor from tf2_ros.transform_broadcaster import TransformBroadcaster -import bdai_ros2_wrappers.process as ros_process -from bdai_ros2_wrappers.executors import background -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.tf_listener_wrapper import TFListenerWrapper -from bdai_ros2_wrappers.utilities import namespace_with +import synchros2.process as ros_process +from synchros2.executors import background +from synchros2.node import Node +from synchros2.tf_listener_wrapper import TFListenerWrapper +from synchros2.utilities import namespace_with class TFBroadcasterNode(Node): diff --git a/synchros2/package.xml b/synchros2/package.xml new file mode 100644 index 0000000..7e814a2 --- /dev/null +++ b/synchros2/package.xml @@ -0,0 +1,20 @@ + + + + synchros2 + 1.0.0 + The AI Institute's wrappers for ROS2 + The AI Institute + MIT + + action_msgs + rclpy + + example_interfaces + python3-typing-extensions + python3-pytest + + + ament_python + + diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/py.typed b/synchros2/resource/synchros2 similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/py.typed rename to synchros2/resource/synchros2 diff --git a/synchros2/setup.cfg b/synchros2/setup.cfg new file mode 100644 index 0000000..5f70e82 --- /dev/null +++ b/synchros2/setup.cfg @@ -0,0 +1,6 @@ +# Copyright (c) 2023 Boston Dynamics AI Institute, Inc. All rights reserved. + +[develop] +script_dir=$base/lib/synchros2 +[install] +install_scripts=$base/lib/synchros2 diff --git a/synchros2/setup.py b/synchros2/setup.py new file mode 100644 index 0000000..42c6fde --- /dev/null +++ b/synchros2/setup.py @@ -0,0 +1,23 @@ +# Copyright (c) 2023 Boston Dynamics AI Institute Inc. All rights reserved. + +from setuptools import find_packages, setup + +package_name = "synchros2" + +setup( + name=package_name, + version="1.0.0", + packages=find_packages(exclude=["test"]), + package_data={package_name: ["py.typed"]}, + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ], + install_requires=["setuptools"], + maintainer="The AI Institute", + maintainer_email="engineering@theaiinstitute.com", + description="The AI Institute's wrappers for ROS2", + tests_require=["pytest"], + zip_safe=True, + license="MIT", +) diff --git a/synchros2/synchros2/__init__.py b/synchros2/synchros2/__init__.py new file mode 100644 index 0000000..6d61717 --- /dev/null +++ b/synchros2/synchros2/__init__.py @@ -0,0 +1 @@ +# Copyright (c) 2023 Boston Dynamics AI Institute Inc. All rights reserved. diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/action.py b/synchros2/synchros2/action.py similarity index 98% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/action.py rename to synchros2/synchros2/action.py index 6f8ba70..4cd8d1e 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/action.py +++ b/synchros2/synchros2/action.py @@ -8,10 +8,10 @@ from rclpy.node import Node from rclpy.task import Future -import bdai_ros2_wrappers.scope as scope -from bdai_ros2_wrappers.callables import ComposableCallable, ComposedCallable, VectorizedCallable, VectorizingCallable -from bdai_ros2_wrappers.futures import FutureConvertible, FutureLike, wait_for_future -from bdai_ros2_wrappers.utilities import Tape +import synchros2.scope as scope +from synchros2.callables import ComposableCallable, ComposedCallable, VectorizedCallable, VectorizingCallable +from synchros2.futures import FutureConvertible, FutureLike, wait_for_future +from synchros2.utilities import Tape class ActionException(Exception): diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/action_client.py b/synchros2/synchros2/action_client.py similarity index 89% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/action_client.py rename to synchros2/synchros2/action_client.py index 734ef99..85e0705 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/action_client.py +++ b/synchros2/synchros2/action_client.py @@ -4,8 +4,8 @@ import rclpy.action from rclpy.node import Node -import bdai_ros2_wrappers.scope as scope -from bdai_ros2_wrappers.action_handle import ActionHandle +import synchros2.scope as scope +from synchros2.action_handle import ActionHandle class ActionClientWrapper(rclpy.action.ActionClient): @@ -46,8 +46,8 @@ def send_goal_and_wait( Args: action_name (str): A representative name of the action for logging goal (Any): The Action Goal sent to the action server - timeout_sec (Optional[float]): A timeout for waiting on a response/result from the action server. Setting to - None creates no timeout + timeout_sec (Optional[float]): A timeout for waiting on a response/result + from the action server. Setting to None creates no timeout Returns: Optional[Any]: @@ -94,12 +94,12 @@ def send_goal_async_handle( Args: action_name (str): A representative name of the action for logging goal (Action.Goal): The Action Goal sent to the action server - result_callback (Optional[Callable[[Action.Result], None]]): A callback to process/handle the resulting - feedback from executing the command - feedback_callback (Optional[Callable[[Action.Feedback], None]]): A callback to process/handle the feedback - received during the execution of the command - on_failure_callback (Optional[Callable[[None], None]]): A callback to process/handle when the action fails - for various reasons + result_callback (Optional[Callable[[Action.Result], None]]): A callback to process/handle + the resulting feedback from executing the command + feedback_callback (Optional[Callable[[Action.Feedback], None]]): A callback to process/handle + the feedback received during the execution of the command + on_failure_callback (Optional[Callable[[None], None]]): A callback to process/handle when + the action fails for various reasons Returns: ActionHandle: An object to manage the asynchronous lifecycle of the action request diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/action_handle.py b/synchros2/synchros2/action_handle.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/action_handle.py rename to synchros2/synchros2/action_handle.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/callables.py b/synchros2/synchros2/callables.py similarity index 98% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/callables.py rename to synchros2/synchros2/callables.py index f7dfb91..0aea0b4 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/callables.py +++ b/synchros2/synchros2/callables.py @@ -21,9 +21,9 @@ from rclpy.task import Future -from bdai_ros2_wrappers.executors import assign_coroutine -from bdai_ros2_wrappers.futures import AnyFuture, FutureLike, as_proper_future -from bdai_ros2_wrappers.utilities import fqn, take_kwargs +from synchros2.executors import assign_coroutine +from synchros2.futures import AnyFuture, FutureLike, as_proper_future +from synchros2.utilities import fqn, take_kwargs def starmap_async(func: Callable[..., AnyFuture], iterable: Iterable[Tuple[Any, ...]]) -> Future: diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/callback_groups.py b/synchros2/synchros2/callback_groups.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/callback_groups.py rename to synchros2/synchros2/callback_groups.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/context.py b/synchros2/synchros2/context.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/context.py rename to synchros2/synchros2/context.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/executors.py b/synchros2/synchros2/executors.py similarity index 99% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/executors.py rename to synchros2/synchros2/executors.py index caea542..66ace84 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/executors.py +++ b/synchros2/synchros2/executors.py @@ -16,8 +16,8 @@ import rclpy.executors import rclpy.node -from bdai_ros2_wrappers.futures import FutureLike -from bdai_ros2_wrappers.utilities import bind_to_thread, fqn +from synchros2.futures import FutureLike +from synchros2.utilities import bind_to_thread, fqn class AutoScalingThreadPool(concurrent.futures.Executor): @@ -411,8 +411,8 @@ def submit( # type: ignore """Submits work to the pool. Args: - fn: a callable to execute. Must be immutable and hashable for - the pool to track concurrent submissions and apply quotas. + fn: a callable to execute. Must be immutable and hashable + for the pool to track concurrent submissions and apply quotas. args: optional positional arguments to forward. kwargs: optional keyword arguments to forward. diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/feeds.py b/synchros2/synchros2/feeds.py similarity index 98% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/feeds.py rename to synchros2/synchros2/feeds.py index fa5d61e..4637b85 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/feeds.py +++ b/synchros2/synchros2/feeds.py @@ -17,16 +17,16 @@ import tf2_ros from rclpy.node import Node -import bdai_ros2_wrappers.scope as scope -from bdai_ros2_wrappers.filters import ( +import synchros2.scope as scope +from synchros2.filters import ( Adapter, ApproximateTimeSynchronizer, Filter, TransformFilter, Tunnel, ) -from bdai_ros2_wrappers.futures import FutureLike -from bdai_ros2_wrappers.utilities import Tape +from synchros2.futures import FutureLike +from synchros2.utilities import Tape MessageT = TypeVar("MessageT") diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/filters.py b/synchros2/synchros2/filters.py similarity index 99% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/filters.py rename to synchros2/synchros2/filters.py index 62dfa7f..9bb3419 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/filters.py +++ b/synchros2/synchros2/filters.py @@ -15,7 +15,7 @@ from rclpy.task import Future from rclpy.time import Time -from bdai_ros2_wrappers.logging import RcutilsLogger +from synchros2.logging import RcutilsLogger class SimpleFilterProtocol(Protocol): diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/futures.py b/synchros2/synchros2/futures.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/futures.py rename to synchros2/synchros2/futures.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/launch/__init__.py b/synchros2/synchros2/launch/__init__.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/launch/__init__.py rename to synchros2/synchros2/launch/__init__.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/launch/actions.py b/synchros2/synchros2/launch/actions.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/launch/actions.py rename to synchros2/synchros2/launch/actions.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/launch/arguments.py b/synchros2/synchros2/launch/arguments.py similarity index 94% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/launch/arguments.py rename to synchros2/synchros2/launch/arguments.py index e028c0a..eda345c 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/launch/arguments.py +++ b/synchros2/synchros2/launch/arguments.py @@ -7,7 +7,7 @@ from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration -from bdai_ros2_wrappers.launch.actions import DeclareBooleanLaunchArgument +from synchros2.launch.actions import DeclareBooleanLaunchArgument _ROBOT_NAME: Literal["robot_name"] = "robot_name" _VERBOSE: Literal["verbose"] = "verbose" diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/launch/substitutions.py b/synchros2/synchros2/launch/substitutions.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/launch/substitutions.py rename to synchros2/synchros2/launch/substitutions.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/logging.py b/synchros2/synchros2/logging.py similarity index 99% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/logging.py rename to synchros2/synchros2/logging.py index d304fab..81946e2 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/logging.py +++ b/synchros2/synchros2/logging.py @@ -18,7 +18,7 @@ from rclpy.logging import LoggingSeverity from rclpy.node import Node -from bdai_ros2_wrappers.utilities import cap, skip, throttle +from synchros2.utilities import cap, skip, throttle SEVERITY_MAP = { logging.NOTSET: LoggingSeverity.UNSET, diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/node.py b/synchros2/synchros2/node.py similarity index 94% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/node.py rename to synchros2/synchros2/node.py index bd2c1ae..ec76607 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/node.py +++ b/synchros2/synchros2/node.py @@ -8,8 +8,8 @@ from rclpy.node import Node as BaseNode from rclpy.waitable import Waitable -from bdai_ros2_wrappers.callback_groups import NonReentrantCallbackGroup -from bdai_ros2_wrappers.logging import MemoizingRcutilsLogger, as_memoizing_logger +from synchros2.callback_groups import NonReentrantCallbackGroup +from synchros2.logging import MemoizingRcutilsLogger, as_memoizing_logger def suppressed(exception: Type[BaseException], func: Callable) -> Callable: diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/process.py b/synchros2/synchros2/process.py similarity index 98% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/process.py rename to synchros2/synchros2/process.py index 545ea7e..81b3727 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/process.py +++ b/synchros2/synchros2/process.py @@ -18,11 +18,11 @@ from rclpy.validate_namespace import validate_namespace from rclpy.validate_node_name import validate_node_name -import bdai_ros2_wrappers.context as context -import bdai_ros2_wrappers.scope as scope -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.tf_listener_wrapper import TFListenerWrapper -from bdai_ros2_wrappers.utilities import either_or +import synchros2.context as context +import synchros2.scope as scope +from synchros2.scope import ROSAwareScope +from synchros2.tf_listener_wrapper import TFListenerWrapper +from synchros2.utilities import either_or NodeT = typing.TypeVar("NodeT", bound=rclpy.node.Node) NodeFactoryCallable = typing.Callable[..., NodeT] diff --git a/proto2ros/proto2ros/__init__.py b/synchros2/synchros2/py.typed similarity index 100% rename from proto2ros/proto2ros/__init__.py rename to synchros2/synchros2/py.typed diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/scope.py b/synchros2/synchros2/scope.py similarity index 97% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/scope.py rename to synchros2/synchros2/scope.py index 97f8848..a109f4f 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/scope.py +++ b/synchros2/synchros2/scope.py @@ -11,11 +11,11 @@ import rclpy.signals import rclpy.utilities -from bdai_ros2_wrappers.executors import AutoScalingMultiThreadedExecutor, background, foreground -from bdai_ros2_wrappers.logging import logs_to_ros -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.tf_listener_wrapper import TFListenerWrapper -from bdai_ros2_wrappers.utilities import fqn, namespace_with +from synchros2.executors import AutoScalingMultiThreadedExecutor, background, foreground +from synchros2.logging import logs_to_ros +from synchros2.node import Node +from synchros2.tf_listener_wrapper import TFListenerWrapper +from synchros2.utilities import fqn, namespace_with NodeT = typing.TypeVar("NodeT", bound=rclpy.node.Node) NodeFactoryCallable = typing.Callable[..., NodeT] @@ -36,7 +36,7 @@ class LocalStack(threading.local): local = LocalStack() - def __init__( + def __init__( # noqa: D417 self, *, global_: bool = False, @@ -47,12 +47,12 @@ def __init__( namespace: typing.Optional[typing.Union[typing.Literal[True], str]] = None, context: typing.Optional[rclpy.context.Context] = None, ) -> None: - """Initializes the ROS 2 aware scope. + r"""Initializes the ROS 2 aware scope. Args: prebaked: whether to include an implicit main node in the scope graph or not, for convenience. May also specify the exact name for the implicit node. - global_: whether to make this scope global (ie. accessible from all threads). + global\_: whether to make this scope global (ie. accessible from all threads). Only one, outermost global scope can be entered at any given time. Global scopes can only be entered from the main thread. namespace: optional namespace for all underlying nodes. Defaults to @@ -482,7 +482,7 @@ def spin( @contextlib.contextmanager -def top( +def top( # noqa: D417 args: typing.Optional[typing.Sequence[str]] = None, *, context: typing.Optional[rclpy.context.Context] = None, @@ -491,13 +491,13 @@ def top( domain_id: typing.Optional[int] = None, **kwargs: typing.Any, ) -> typing.Iterator[ROSAwareScope]: - """Manages a ROS 2 aware scope, handling ROS 2 context lifecycle as well. + r"""Manages a ROS 2 aware scope, handling ROS 2 context lifecycle as well. Args: args: optional command-line arguments for context initialization. context: optional context to manage. If none is provided, one will be created. For global scopes, the default context will be used. - global_: whether to use the global context or a locally constructed one. + global\_: whether to use the global context or a locally constructed one. interruptible: global interruptible scopes will skip installing ROS 2 signal handlers and let the user deal with SIGINT and SIGTERM interruptions instead. diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/service.py b/synchros2/synchros2/service.py similarity index 97% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/service.py rename to synchros2/synchros2/service.py index e088c71..4b13b25 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/service.py +++ b/synchros2/synchros2/service.py @@ -6,9 +6,9 @@ from rclpy.node import Node from rclpy.task import Future -import bdai_ros2_wrappers.scope as scope -from bdai_ros2_wrappers.callables import ComposableCallable, VectorizingCallable -from bdai_ros2_wrappers.futures import FutureLike, wait_for_future +import synchros2.scope as scope +from synchros2.callables import ComposableCallable, VectorizingCallable +from synchros2.futures import FutureLike, wait_for_future class ServiceException(Exception): diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/service_handle.py b/synchros2/synchros2/service_handle.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/service_handle.py rename to synchros2/synchros2/service_handle.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/single_goal_action_server.py b/synchros2/synchros2/single_goal_action_server.py similarity index 86% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/single_goal_action_server.py rename to synchros2/synchros2/single_goal_action_server.py index f3b5ef6..05e5ee7 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/single_goal_action_server.py +++ b/synchros2/synchros2/single_goal_action_server.py @@ -5,8 +5,8 @@ from rclpy.callback_groups import CallbackGroup from rclpy.node import Node -from bdai_ros2_wrappers.single_goal_multiple_action_servers import SingleGoalMultipleActionServers -from bdai_ros2_wrappers.type_hints import ActionType +from synchros2.single_goal_multiple_action_servers import SingleGoalMultipleActionServers +from synchros2.type_hints import ActionType # Note: for this to work correctly you must use a multi-threaded executor when spinning the node! diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/single_goal_multiple_action_servers.py b/synchros2/synchros2/single_goal_multiple_action_servers.py similarity index 97% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/single_goal_multiple_action_servers.py rename to synchros2/synchros2/single_goal_multiple_action_servers.py index b9d0f24..099c6f6 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/single_goal_multiple_action_servers.py +++ b/synchros2/synchros2/single_goal_multiple_action_servers.py @@ -8,8 +8,8 @@ from rclpy.impl.rcutils_logger import RcutilsLogger from rclpy.node import Node -from bdai_ros2_wrappers.type_hints import ActionType -from bdai_ros2_wrappers.utilities import synchronized +from synchros2.type_hints import ActionType +from synchros2.utilities import synchronized class SingleGoalMultipleActionServers: diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/static_transform_broadcaster.py b/synchros2/synchros2/static_transform_broadcaster.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/static_transform_broadcaster.py rename to synchros2/synchros2/static_transform_broadcaster.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/subscription.py b/synchros2/synchros2/subscription.py similarity index 97% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/subscription.py rename to synchros2/synchros2/subscription.py index 59aa39d..808a625 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/subscription.py +++ b/synchros2/synchros2/subscription.py @@ -8,10 +8,10 @@ from rclpy.qos import QoSProfile from rclpy.task import Future -import bdai_ros2_wrappers.scope as scope -from bdai_ros2_wrappers.feeds import MessageFeed -from bdai_ros2_wrappers.filters import ApproximateTimeSynchronizer, Subscriber -from bdai_ros2_wrappers.futures import FutureLike, wait_for_future +import synchros2.scope as scope +from synchros2.feeds import MessageFeed +from synchros2.filters import ApproximateTimeSynchronizer, Subscriber +from synchros2.futures import FutureLike, wait_for_future MessageT = TypeVar("MessageT") diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/tf_listener_wrapper.py b/synchros2/synchros2/tf_listener_wrapper.py similarity index 85% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/tf_listener_wrapper.py rename to synchros2/synchros2/tf_listener_wrapper.py index 5b393a7..63bff2b 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/tf_listener_wrapper.py +++ b/synchros2/synchros2/tf_listener_wrapper.py @@ -12,7 +12,7 @@ from tf2_ros.buffer import Buffer from tf2_ros.transform_listener import TransformListener -from bdai_ros2_wrappers.futures import wait_for_future +from synchros2.futures import wait_for_future @runtime_checkable @@ -40,16 +40,20 @@ def from_time_like(obj: Union[StampLike, TimeLike]) -> Time: class TFListenerWrapper: - """A `tf2_ros` lookup device, wrapping both a buffer and a listener. + """A ``tf2_ros`` lookup device, wrapping both a buffer and a listener. When using process-wide machinery: + .. code-block:: python + tf_listener = TFListenerWrapper() tf_listener.wait_for_a_tform_b(target_frame, source_frame) tf_listener.lookup_a_tform_b(target_frame, source_frame) When composed by a ROS 2 node: + .. code-block:: python + class MyNode(Node): def __init__(self, **kwargs: Any) -> None: @@ -71,7 +75,7 @@ def __init__(self, node: Optional[Node] = None, cache_time_s: Optional[float] = node: optional node for transform listening, defaults to the current scope node. cache_time_s: optional transform buffer size, in seconds. """ - import bdai_ros2_wrappers.scope as scope # locally to avoid circular import + import synchros2.scope as scope # locally to avoid circular import if node is None: node = scope.ensure_node() @@ -103,7 +107,8 @@ def wait_for_a_tform_b_async( frame_a: Base frame for transform. The transform returned will be frame_a_t_frame_b frame_b: Tip frame for transform. The transform returned will be frame_a_t_frame_b transform_time: The time at which to look up the transform. If left at None, the most - recent transform available will used. + recent transform available will used. + Returns: A future that will tell if a transform between frame_a and frame_b is available at the time specified. """ @@ -126,10 +131,11 @@ def wait_for_a_tform_b( frame_a: Base frame for transform. The transform returned will be frame_a_t_frame_b frame_b: Tip frame for transform. The transform returned will be frame_a_t_frame_b transform_time: The time at which to look up the transform. If left at None, the most - recent transform available will used. + recent transform available will used. timeout_sec: The time to wait for the transform to become available if the requested time is beyond - the most recent transform in the buffer. If set to 0, it will not wait. If left at None, it will - wait indefinitely. + the most recent transform in the buffer. If set to 0, it will not wait. If left at None, it will + wait indefinitely. + Returns: Whether a transform between frame_a and frame_b is available at the specified time. """ @@ -157,17 +163,19 @@ def lookup_a_tform_b( Args: frame_a: Base frame for transform. The transform returned will be frame_a_t_frame_b frame_b: Tip frame for transform. The transform returned will be frame_a_t_frame_b - transform_time: The time at which to look up the transform. If left at None, the most - recent transform available will used. + transform_time: The time at which to look up the transform. If left at None, + the most recent transform available will used. timeout_sec: The time to wait for the transform to become available if the requested time is beyond - the most recent transform in the buffer. If set to 0, it will not wait. If left at None, it will - wait indefinitely. + the most recent transform in the buffer. If set to 0, it will not wait. If left at None, it will + wait indefinitely. wait_for_frames: If true, it will wait for a path to exist from frame_a to frame_b in the - buffer. If false, lookup will fail immediately if a path between frames does not exist, - regardless of what timeout was set. Note that wait_for_a_tform_b can also be used to - wait for a transform to become available. + buffer. If false, lookup will fail immediately if a path between frames does not exist, + regardless of what timeout was set. Note that wait_for_a_tform_b can also be used to + wait for a transform to become available. + Returns: The transform frame_a_t_frame_b at the time specified. + Raises: All the possible TransformExceptions. """ @@ -205,14 +213,16 @@ def lookup_latest_timestamp( frame_a: Base frame for transform. The transform returned will be frame_a_t_frame_b frame_b: Tip frame for transform. The transform returned will be frame_a_t_frame_b timeout_sec: The time to wait for the transform to become available if the requested time is beyond - the most recent transform in the buffer. If set to 0, it will not wait. If left at None, it will - wait indefinitely. + the most recent transform in the buffer. If set to 0, it will not wait. If left at None, it will + wait indefinitely. wait_for_frames: If true, it will wait for a path to exist from frame_a to frame_b in the - buffer. If false, lookup will fail immediately if a path between frames does not exist, - regardless of what timeout was set. Note that wait_for_a_tform_b can also be used to - wait for a transform to become available. + buffer. If false, lookup will fail immediately if a path between frames does not exist, + regardless of what timeout was set. Note that wait_for_a_tform_b can also be used to + wait for a transform to become available. + Returns: The timestamp from the latest recorded transform frame_a_t_frame_b + Raises: All the possible TransformExceptions. """ diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/time.py b/synchros2/synchros2/time.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/time.py rename to synchros2/synchros2/time.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/type_hints.py b/synchros2/synchros2/type_hints.py similarity index 100% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/type_hints.py rename to synchros2/synchros2/type_hints.py diff --git a/bdai_ros2_wrappers/bdai_ros2_wrappers/utilities.py b/synchros2/synchros2/utilities.py similarity index 97% rename from bdai_ros2_wrappers/bdai_ros2_wrappers/utilities.py rename to synchros2/synchros2/utilities.py index e5f094b..4287912 100644 --- a/bdai_ros2_wrappers/bdai_ros2_wrappers/utilities.py +++ b/synchros2/synchros2/utilities.py @@ -16,7 +16,7 @@ import rclpy.time from rclpy.task import Future -from bdai_ros2_wrappers.futures import FutureLike +from synchros2.futures import FutureLike T = TypeVar("T") U = TypeVar("U") @@ -404,15 +404,19 @@ def synchronized( This function can be used as a decorator, like: - @synchronized - def my_function(...): - ... + .. code-block:: python - or + @synchronized + def my_function(...): + ... - @synchronized(lock=my_lock) - def my_function(...): - ... + or: + + .. code-block:: python + + @synchronized(lock=my_lock) + def my_function(...): + ... """ if lock is None: lock = threading.Lock() @@ -436,21 +440,27 @@ def functional_decorator(base_decorator: Callable) -> Callable: As an example, consider the following decorator example: - @functional_decorator - def my_decorator(func, some_flag=None): - ... + .. code-block:: python + + @functional_decorator + def my_decorator(func, some_flag=None): + ... This decorator can then be used like this: - @my_decorator - def my_function(*args): - ... + .. code-block:: python + + @my_decorator + def my_function(*args): + ... and also like this: - @my_decorator(some_flag=True) - def my_function(*args): - ... + .. code-block:: python + + @my_decorator(some_flag=True) + def my_function(*args): + ... """ @functools.wraps(base_decorator) diff --git a/bdai_ros2_wrappers/test/conftest.py b/synchros2/test/conftest.py similarity index 88% rename from bdai_ros2_wrappers/test/conftest.py rename to synchros2/test/conftest.py index 98074ce..2e05b7e 100644 --- a/bdai_ros2_wrappers/test/conftest.py +++ b/synchros2/test/conftest.py @@ -5,8 +5,8 @@ import domain_coordinator import pytest -import bdai_ros2_wrappers.scope as scope -from bdai_ros2_wrappers.scope import ROSAwareScope +import synchros2.scope as scope +from synchros2.scope import ROSAwareScope @pytest.fixture diff --git a/bdai_ros2_wrappers/test/launch/test_actions.py b/synchros2/test/launch/test_actions.py similarity index 96% rename from bdai_ros2_wrappers/test/launch/test_actions.py rename to synchros2/test/launch/test_actions.py index 7235e6c..d61ac70 100644 --- a/bdai_ros2_wrappers/test/launch/test_actions.py +++ b/synchros2/test/launch/test_actions.py @@ -3,7 +3,7 @@ import pytest -from bdai_ros2_wrappers.launch.actions import DeclareBooleanLaunchArgument, DeclareEnumLaunchArgument +from synchros2.launch.actions import DeclareBooleanLaunchArgument, DeclareEnumLaunchArgument def test_declare_boolean_launch_argument_default_value_false() -> None: diff --git a/bdai_ros2_wrappers/test/test_action.py b/synchros2/test/test_action.py similarity index 98% rename from bdai_ros2_wrappers/test/test_action.py rename to synchros2/test/test_action.py index 1c2057e..76d5aad 100644 --- a/bdai_ros2_wrappers/test/test_action.py +++ b/synchros2/test/test_action.py @@ -14,12 +14,12 @@ from rclpy.executors import SingleThreadedExecutor from typing_extensions import TypeAlias -import bdai_ros2_wrappers.scope as ros_scope -from bdai_ros2_wrappers.action import Actionable, ActionAborted, ActionCancelled, ActionRejected -from bdai_ros2_wrappers.executors import foreground -from bdai_ros2_wrappers.futures import wait_for_future -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.scope import ROSAwareScope +import synchros2.scope as ros_scope +from synchros2.action import Actionable, ActionAborted, ActionCancelled, ActionRejected +from synchros2.executors import foreground +from synchros2.futures import wait_for_future +from synchros2.node import Node +from synchros2.scope import ROSAwareScope def fibonacci_sequence(order: int) -> Iterable[int]: diff --git a/bdai_ros2_wrappers/test/test_action_client.py b/synchros2/test/test_action_client.py similarity index 94% rename from bdai_ros2_wrappers/test/test_action_client.py rename to synchros2/test/test_action_client.py index be9663f..2a5d803 100644 --- a/bdai_ros2_wrappers/test/test_action_client.py +++ b/synchros2/test/test_action_client.py @@ -6,9 +6,9 @@ from example_interfaces.action import Fibonacci from rclpy.action.server import ActionServer, GoalResponse, ServerGoalHandle -from bdai_ros2_wrappers.action_client import ActionClientWrapper -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.scope import ROSAwareScope +from synchros2.action_client import ActionClientWrapper +from synchros2.node import Node +from synchros2.scope import ROSAwareScope class FibonacciActionServer(ActionServer): diff --git a/bdai_ros2_wrappers/test/test_action_handle.py b/synchros2/test/test_action_handle.py similarity index 98% rename from bdai_ros2_wrappers/test/test_action_handle.py rename to synchros2/test/test_action_handle.py index 2f89a4f..0fef29c 100644 --- a/bdai_ros2_wrappers/test/test_action_handle.py +++ b/synchros2/test/test_action_handle.py @@ -10,8 +10,8 @@ from rclpy.action.server import CancelResponse, GoalResponse, ServerGoalHandle from rclpy.node import Node -from bdai_ros2_wrappers.action_handle import ActionHandle -from bdai_ros2_wrappers.scope import ROSAwareScope +from synchros2.action_handle import ActionHandle +from synchros2.scope import ROSAwareScope def _default_execute_callback(goal_handle: ServerGoalHandle) -> Fibonacci.Result: diff --git a/bdai_ros2_wrappers/test/test_callables.py b/synchros2/test/test_callables.py similarity index 97% rename from bdai_ros2_wrappers/test/test_callables.py rename to synchros2/test/test_callables.py index 74ce38e..7b4cc06 100644 --- a/bdai_ros2_wrappers/test/test_callables.py +++ b/synchros2/test/test_callables.py @@ -5,9 +5,9 @@ import pytest -from bdai_ros2_wrappers.callables import GeneralizedGuard, generalized_method -from bdai_ros2_wrappers.futures import wait_for_future -from bdai_ros2_wrappers.scope import ROSAwareScope +from synchros2.callables import GeneralizedGuard, generalized_method +from synchros2.futures import wait_for_future +from synchros2.scope import ROSAwareScope class Bucket: diff --git a/bdai_ros2_wrappers/test/test_context.py b/synchros2/test/test_context.py similarity index 78% rename from bdai_ros2_wrappers/test/test_context.py rename to synchros2/test/test_context.py index 05a35ad..8359086 100644 --- a/bdai_ros2_wrappers/test/test_context.py +++ b/synchros2/test/test_context.py @@ -2,8 +2,8 @@ import rclpy -from bdai_ros2_wrappers.context import wait_for_shutdown -from bdai_ros2_wrappers.process import ROSAwareScope +from synchros2.context import wait_for_shutdown +from synchros2.process import ROSAwareScope def test_wait_for_shutdown(ros: ROSAwareScope) -> None: diff --git a/bdai_ros2_wrappers/test/test_executors.py b/synchros2/test/test_executors.py similarity index 97% rename from bdai_ros2_wrappers/test/test_executors.py rename to synchros2/test/test_executors.py index fa95525..f30f24e 100644 --- a/bdai_ros2_wrappers/test/test_executors.py +++ b/synchros2/test/test_executors.py @@ -11,8 +11,8 @@ from rclpy.node import Node from std_srvs.srv import Trigger -from bdai_ros2_wrappers.executors import AutoScalingMultiThreadedExecutor, AutoScalingThreadPool, background -from bdai_ros2_wrappers.futures import wait_for_future +from synchros2.executors import AutoScalingMultiThreadedExecutor, AutoScalingThreadPool, background +from synchros2.futures import wait_for_future @pytest.fixture diff --git a/bdai_ros2_wrappers/test/test_feeds.py b/synchros2/test/test_feeds.py similarity index 96% rename from bdai_ros2_wrappers/test/test_feeds.py rename to synchros2/test/test_feeds.py index 9bdc2ac..b9f4376 100644 --- a/bdai_ros2_wrappers/test/test_feeds.py +++ b/synchros2/test/test_feeds.py @@ -10,15 +10,15 @@ TwistStamped, ) -from bdai_ros2_wrappers.feeds import ( +from synchros2.feeds import ( AdaptedMessageFeed, FramedMessageFeed, MessageFeed, SynchronizedMessageFeed, ) -from bdai_ros2_wrappers.filters import Filter -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.utilities import ensure +from synchros2.filters import Filter +from synchros2.scope import ROSAwareScope +from synchros2.utilities import ensure def test_framed_message_feed(ros: ROSAwareScope) -> None: diff --git a/bdai_ros2_wrappers/test/test_filters.py b/synchros2/test/test_filters.py similarity index 98% rename from bdai_ros2_wrappers/test/test_filters.py rename to synchros2/test/test_filters.py index 1c87042..9d3dc73 100644 --- a/bdai_ros2_wrappers/test/test_filters.py +++ b/synchros2/test/test_filters.py @@ -5,7 +5,7 @@ import tf2_ros from geometry_msgs.msg import PoseStamped, TransformStamped -from bdai_ros2_wrappers.filters import Filter, TransformFilter +from synchros2.filters import Filter, TransformFilter def test_transform_wait() -> None: diff --git a/bdai_ros2_wrappers/test/test_futures.py b/synchros2/test/test_futures.py similarity index 78% rename from bdai_ros2_wrappers/test/test_futures.py rename to synchros2/test/test_futures.py index 129a0e9..17e6a5e 100644 --- a/bdai_ros2_wrappers/test/test_futures.py +++ b/synchros2/test/test_futures.py @@ -2,8 +2,8 @@ from rclpy.task import Future -from bdai_ros2_wrappers.futures import wait_for_future -from bdai_ros2_wrappers.scope import ROSAwareScope +from synchros2.futures import wait_for_future +from synchros2.scope import ROSAwareScope def test_wait_for_cancelled_future(ros: ROSAwareScope) -> None: diff --git a/bdai_ros2_wrappers/test/test_integration.py b/synchros2/test/test_integration.py similarity index 97% rename from bdai_ros2_wrappers/test/test_integration.py rename to synchros2/test/test_integration.py index 37b3ced..485d163 100644 --- a/bdai_ros2_wrappers/test/test_integration.py +++ b/synchros2/test/test_integration.py @@ -13,9 +13,9 @@ from rclpy.duration import Duration from rclpy.time import Time -from bdai_ros2_wrappers.futures import wait_for_future -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.scope import ROSAwareScope +from synchros2.futures import wait_for_future +from synchros2.node import Node +from synchros2.scope import ROSAwareScope class MinimalTransformPublisher(Node): diff --git a/bdai_ros2_wrappers/test/test_logging.py b/synchros2/test/test_logging.py similarity index 92% rename from bdai_ros2_wrappers/test/test_logging.py rename to synchros2/test/test_logging.py index d90d50c..137ccb2 100644 --- a/bdai_ros2_wrappers/test/test_logging.py +++ b/synchros2/test/test_logging.py @@ -7,10 +7,10 @@ from rclpy.clock import ROSClock from rclpy.time import Time -from bdai_ros2_wrappers.futures import unwrap_future -from bdai_ros2_wrappers.logging import LoggingSeverity, as_memoizing_logger, logs_to_ros -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.subscription import Subscription +from synchros2.futures import unwrap_future +from synchros2.logging import LoggingSeverity, as_memoizing_logger, logs_to_ros +from synchros2.scope import ROSAwareScope +from synchros2.subscription import Subscription def test_memoizing_logger(verbose_ros: ROSAwareScope) -> None: diff --git a/bdai_ros2_wrappers/test/test_node.py b/synchros2/test/test_node.py similarity index 95% rename from bdai_ros2_wrappers/test/test_node.py rename to synchros2/test/test_node.py index a9e25d6..7318746 100644 --- a/bdai_ros2_wrappers/test/test_node.py +++ b/synchros2/test/test_node.py @@ -9,9 +9,9 @@ from rclpy.context import Context from std_srvs.srv import Trigger -from bdai_ros2_wrappers.executors import AutoScalingMultiThreadedExecutor -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.scope import ROSAwareScope +from synchros2.executors import AutoScalingMultiThreadedExecutor +from synchros2.node import Node +from synchros2.scope import ROSAwareScope @pytest.fixture diff --git a/bdai_ros2_wrappers/test/test_process.py b/synchros2/test/test_process.py similarity index 93% rename from bdai_ros2_wrappers/test/test_process.py rename to synchros2/test/test_process.py index 0d66e46..e24320e 100644 --- a/bdai_ros2_wrappers/test/test_process.py +++ b/synchros2/test/test_process.py @@ -5,8 +5,8 @@ from geometry_msgs.msg import TransformStamped from std_srvs.srv import Trigger -import bdai_ros2_wrappers.process as process -from bdai_ros2_wrappers.static_transform_broadcaster import StaticTransformBroadcaster +import synchros2.process as process +from synchros2.static_transform_broadcaster import StaticTransformBroadcaster def test_process_wrapping() -> None: @@ -91,6 +91,6 @@ def main(args: argparse.Namespace) -> int: assert args.robot == "spot" return 0 - with mock.patch("bdai_ros2_wrappers.scope.logs_to_ros") as logs_to_ros: + with mock.patch("synchros2.scope.logs_to_ros") as logs_to_ros: assert main(["test_command", "spot", "--quiet"]) == 0 assert not logs_to_ros.called diff --git a/bdai_ros2_wrappers/test/test_service.py b/synchros2/test/test_service.py similarity index 94% rename from bdai_ros2_wrappers/test/test_service.py rename to synchros2/test/test_service.py index 2dc07bd..01802cc 100644 --- a/bdai_ros2_wrappers/test/test_service.py +++ b/synchros2/test/test_service.py @@ -4,9 +4,9 @@ from rclpy.duration import Duration from std_srvs.srv import Trigger -from bdai_ros2_wrappers.futures import wait_for_future -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.service import Serviced, ServiceError, ServiceTimeout +from synchros2.futures import wait_for_future +from synchros2.scope import ROSAwareScope +from synchros2.service import Serviced, ServiceError, ServiceTimeout def succeeding_callback(request: Trigger.Request, response: Trigger.Response) -> Trigger.Response: diff --git a/bdai_ros2_wrappers/test/test_service_handle.py b/synchros2/test/test_service_handle.py similarity index 97% rename from bdai_ros2_wrappers/test/test_service_handle.py rename to synchros2/test/test_service_handle.py index 425d4df..cd95baf 100644 --- a/bdai_ros2_wrappers/test/test_service_handle.py +++ b/synchros2/test/test_service_handle.py @@ -5,8 +5,8 @@ from rclpy.client import Client from std_srvs.srv import Empty, SetBool, Trigger -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.service_handle import ServiceHandle +from synchros2.scope import ROSAwareScope +from synchros2.service_handle import ServiceHandle def do_request( diff --git a/bdai_ros2_wrappers/test/test_single_goal_action_server.py b/synchros2/test/test_single_goal_action_server.py similarity index 85% rename from bdai_ros2_wrappers/test/test_single_goal_action_server.py rename to synchros2/test/test_single_goal_action_server.py index f0c8e5f..ac2f1e1 100644 --- a/bdai_ros2_wrappers/test/test_single_goal_action_server.py +++ b/synchros2/test/test_single_goal_action_server.py @@ -4,9 +4,9 @@ from example_interfaces.action import Fibonacci from rclpy.action.server import ServerGoalHandle -from bdai_ros2_wrappers.action_client import ActionClientWrapper -from bdai_ros2_wrappers.process import ROSAwareScope -from bdai_ros2_wrappers.single_goal_action_server import SingleGoalActionServer +from synchros2.action_client import ActionClientWrapper +from synchros2.process import ROSAwareScope +from synchros2.single_goal_action_server import SingleGoalActionServer def test_single_goal_action_server(ros: ROSAwareScope) -> None: diff --git a/bdai_ros2_wrappers/test/test_single_goal_multiple_action_servers.py b/synchros2/test/test_single_goal_multiple_action_servers.py similarity index 94% rename from bdai_ros2_wrappers/test/test_single_goal_multiple_action_servers.py rename to synchros2/test/test_single_goal_multiple_action_servers.py index 68469bf..b47aa0e 100644 --- a/bdai_ros2_wrappers/test/test_single_goal_multiple_action_servers.py +++ b/synchros2/test/test_single_goal_multiple_action_servers.py @@ -8,10 +8,10 @@ from rclpy.action.server import ServerGoalHandle from typing_extensions import TypeAlias -from bdai_ros2_wrappers.action import Actionable -from bdai_ros2_wrappers.futures import wait_for_future -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.single_goal_multiple_action_servers import SingleGoalMultipleActionServers +from synchros2.action import Actionable +from synchros2.futures import wait_for_future +from synchros2.scope import ROSAwareScope +from synchros2.single_goal_multiple_action_servers import SingleGoalMultipleActionServers @pytest.fixture diff --git a/bdai_ros2_wrappers/test/test_static_transform_broadcaster.py b/synchros2/test/test_static_transform_broadcaster.py similarity index 92% rename from bdai_ros2_wrappers/test/test_static_transform_broadcaster.py rename to synchros2/test/test_static_transform_broadcaster.py index 744e3c6..1e79c78 100644 --- a/bdai_ros2_wrappers/test/test_static_transform_broadcaster.py +++ b/synchros2/test/test_static_transform_broadcaster.py @@ -2,9 +2,9 @@ from geometry_msgs.msg import TransformStamped -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.static_transform_broadcaster import StaticTransformBroadcaster -from bdai_ros2_wrappers.tf_listener_wrapper import TFListenerWrapper +from synchros2.scope import ROSAwareScope +from synchros2.static_transform_broadcaster import StaticTransformBroadcaster +from synchros2.tf_listener_wrapper import TFListenerWrapper def test_static_tf_burst(ros: ROSAwareScope) -> None: diff --git a/bdai_ros2_wrappers/test/test_subscription.py b/synchros2/test/test_subscription.py similarity index 96% rename from bdai_ros2_wrappers/test/test_subscription.py rename to synchros2/test/test_subscription.py index 513ab67..a219d73 100644 --- a/bdai_ros2_wrappers/test/test_subscription.py +++ b/synchros2/test/test_subscription.py @@ -6,11 +6,11 @@ from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile from std_msgs.msg import Int8, String -from bdai_ros2_wrappers.futures import wait_for_future -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.subscription import Subscription, wait_for_message, wait_for_messages -from bdai_ros2_wrappers.utilities import ensure +from synchros2.futures import wait_for_future +from synchros2.node import Node +from synchros2.scope import ROSAwareScope +from synchros2.subscription import Subscription, wait_for_message, wait_for_messages +from synchros2.utilities import ensure DEFAULT_QOS_PROFILE = QoSProfile( durability=DurabilityPolicy.TRANSIENT_LOCAL, diff --git a/bdai_ros2_wrappers/test/test_tf_listener_wrapper.py b/synchros2/test/test_tf_listener_wrapper.py similarity index 97% rename from bdai_ros2_wrappers/test/test_tf_listener_wrapper.py rename to synchros2/test/test_tf_listener_wrapper.py index a2f34e5..a1087b7 100644 --- a/bdai_ros2_wrappers/test/test_tf_listener_wrapper.py +++ b/synchros2/test/test_tf_listener_wrapper.py @@ -8,9 +8,9 @@ from rclpy.time import Time from tf2_ros import ExtrapolationException, LookupException, TransformBroadcaster -from bdai_ros2_wrappers.node import Node -from bdai_ros2_wrappers.scope import ROSAwareScope -from bdai_ros2_wrappers.tf_listener_wrapper import TFListenerWrapper +from synchros2.node import Node +from synchros2.scope import ROSAwareScope +from synchros2.tf_listener_wrapper import TFListenerWrapper ROBOT = "test_robot" CAMERA = "camera_1" diff --git a/bdai_ros2_wrappers/test/test_utilities.py b/synchros2/test/test_utilities.py similarity index 97% rename from bdai_ros2_wrappers/test/test_utilities.py rename to synchros2/test/test_utilities.py index c3e2d45..028ef8e 100644 --- a/bdai_ros2_wrappers/test/test_utilities.py +++ b/synchros2/test/test_utilities.py @@ -6,7 +6,7 @@ import pytest -from bdai_ros2_wrappers.utilities import Tape, either_or, ensure, namespace_with +from synchros2.utilities import Tape, either_or, ensure, namespace_with def test_tape_head() -> None: