-
 

proto2ros package from proto2ros repo

proto2ros proto2ros_tests

Package Summary

Tags No category tags.
Version 1.0.0
License MIT
Build type AMENT_CMAKE
Use RECOMMENDED

Repository Summary

Checkout URI https://github.com/bdaiinstitute/proto2ros.git
VCS Type git
VCS Version main
Last Updated 2024-12-06
Dev Status DEVELOPED
CI status No Continuous Integration
Released RELEASED
Tags No category tags.
Contributing Help Wanted (0)
Good First Issues (0)
Pull Requests to Review (0)

Package Description

Protobuf to ROS 2 interoperability interfaces

Additional Links

No additional links.

Maintainers

  • Boston Dynamics AI Institute

Authors

No additional authors.

proto2ros

When exposing Protobuf interfaces to ROS 2, proto2ros streamlines message translation and generation, so as to trivially augment rosidl pipeline invocations to take Protobuf definitions and output ROS 2 message definitions. Additionally, message conversion APIs are generated to simplify bridging Protobuf and ROS 2 message code.

Table of contents

Features

Message generation

Protobuf enumeration and message definitions are translated to equivalent ROS 2 message definitions. Protobuf definitions, including comments, are extracted from Protobuf descriptor sets, as generated by protoc. More than one ROS 2 message definition may be necessary to represent a given Protobuf definition, as explained in the map types and one-of fields subsections.

Package mapping

All Protobuf packages to which processed definitions belong are implicitly mapped to the ROS 2 package that will host their ROS 2 message equivalents. As for the rest, the user may specify a package mapping in proto2ros configuration. Whether that is necessary or not depends on how message types are mapped.

Type mapping

Scalar types

The mapping between Protobuf and ROS 2 scalar types is shown below.

Protobuf scalar type name ROS 2 scalar type name
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
bytes uint8[]

Note that, unlike all others, the Protobuf bytes scalar type maps to a sequence type in ROS 2. This is convenient but forces special handling of repeated bytes fields.

Messages types

Every Protobuf message maps to a ROS 2 message. For statically typed message (as opposed to dynamically typed google.protobuf.Any messages), this mapping is derived from the sequential application of the following rules, on first match wins basis:

  • A user-defined message mapping in proto2ros configuration matches. Fully qualified message names are used verbatim.
  • A user-defined or implicit package mapping in proto2ros configuration matches. If multiple matches are found, the longest match applies. Fully qualified message names are camel-cased.
  • Passing unknown Protobuf messages is allowed by proto2ros configuration. Then proto2ros/AnyProto is used.

For google.protobuf.Any messages, the rules for any types apply.

It is through message mappings that ad-hoc equivalences can be established e.g. some Protobuf message may be equivalent to some standard ROS 2 message (and this is already the case for several core Protobuf messages, see default proto2ros configuration). It is through package mappings that equivalences generated by proto2ros in full, for a given ROS 2 package or one of its dependencies, interact with each other.

For example, given the following configuration overlay:

message_mapping:
  third_party.data.Text: std_msgs/String
  google.protobuf.Any: custom_msgs/Any
package_mapping:
  third_party.data: data_msgs
  third_party.data.legacy: data_legacy_msgs

google.protobuf.Any would map to proto2ros/Any, third_party.data.Text would map to std_msgs/String, third_party.data.Blob would map to data_msgs/Blob, third_party.data.legacy.Image would map to data_legacy_msgs/Image, and some_package.Data would map to proto2ros/AnyProto if passthrough_unknown is enabled, raising an error otherwise.

Enum types

As ROS 2 messages lack the notion of enumerated types entirely, a ROS 2 message is generated for each Protobuf enumeration. This ROS 2 message defines a homonymous integer constant for each enum value and a single value integer field to bear it. A sample equivalence is shown below.

Protobuf .proto definition ROS 2 .msg definition
```proto // some.proto enum Status { STATUS_UNKNOWN = 0; STATUS_OK = 1; STATUS_FAILURE = 2; } ``` ```python # Status.msg int32 STATUS_UNKNOWN=0 int32 STATUS_OK=1 int32 STATUS_FAILURE=2 int32 value ```
Map types

Over the wire, Protobuf map types are bound to be equivalent to a sequence of key-value pairs (or map entry messages). For ROS 2, the exact same convention is observed. A ROS 2 message is thus generated for each map entry message. A sample equivalence is shown below.

Protobuf .proto definition Protobuf equivalent syntax ROS 2 .msg definition
```proto // some.proto message Device { map<string, string> attributes = 1; } ``` ```proto // some.proto message Device { message AttributesEntry { string key = 1; string value = 2; } repeated AttributesEntry attributes = 1; } ``` ```python # DeviceAttributesEntry.msg string key string value ```
```python # Device.msg /DeviceAttributesEntry[] attributes ``` </td> </tr> </table> </div> ##### Any types Dynamically typed (i.e. `google.protobuf.Any`) Protobuf messages are mapped to ROS 2 messages as specified by _any expansions_. An any expansion is a type set $T$ that fully characterizes what to expect of a `google.protobuf.Any` message field. These are indexed after Protobuf message name and field name. Cardinality $|T|$ always satisfies $|T| > 0$. Given an applicable any expansion is found: - if $|T| == 1$ and `allow_any_casts` is enabled, the corresponding equivalent ROS 2 message type for that sole Protobuf message type, as dictated by message type mapping rules, will be used (as if that statically typed Protobuf message type had been found in place of `google.protobuf.Any`); - otherwise, a dynamically typed (i.e. `proto2ros/Any`) ROS 2 message is used as the equivalent ROS 2 message type, which will bear the equivalent ROS 2 type set (with $|T| > 0$) as dictated by message type mapping rules. Else, a `proto2ros/AnyProto` ROS 2 message type is used as the equivalent ROS 2 message type, bearing the unmodified, serialized Protobuf message. For example, given the following configuration overlay: ```yaml any_expansions: third_party.data.Storage.params: third_party.data.StorageParams third_party.data.StorageParams.implementation_specific: [third_party.data.S3Params, third_party.data.PGParams] allow_any_casts: true ``` `google.protobuf.Any` for the `params` field in the `third_party.data.Storage` Protobuf message would map to the ROS 2 equivalent, as per message type mapping rules, of the `third_party.data.StorageParams` Protobuf message, whereas `google.protobuf.Any` for the `implementation_specific` field in the `third_party.data.StorageParams` Protobuf message would map to `proto2ros/Any`. Conversion APIs, however, can use the information provided by these any expansions to perform the necessary casting in runtime. #### Recursive types Protobuf supports recursive message definitions but ROS 2 does not. To workaround this limitation, a message dependency graph reflecting the composition relationships between known messages is built and analyzed for cycles. Once a cycle has been identified, it is broken by the weakest link (i.e. the minimal change set) using `proto2ros/Any`, functionally type erasing one or more fields. #### Field mapping ##### Optional fields Optional fields in Protobuf messages, and fields with [explicit presence](https://protobuf.dev/programming-guides/field_presence) tracking in general, are conventionally implemented using a bit mask field in ROS 2 messages. As ROS 2 messages lack the notion of optional fields entirely, an unsigned integer `has_field` field explicitly conveys which message fields bear meaningful information. For each optional field `f`, an unsigned integer constant `F_FIELD_SET` bit mask is defined. Bitwise binary operations can then be used to explicitly indicate and check for field presence. A sample equivalence is shown below.
Protobuf .proto definition ROS 2 .msg definition
```proto // some.proto message Option { optional string value = 1; } ``` ```python // Option.msg uint8 VALUE_FIELD_SET=1 string value uint8 has_field 255 ```
Note that, to match ROS 2 message semantics, the bit mask is fully set by default. That is, all fields are assumed to be present by default. **Implementation note**: bit masks can be 8, 16, 32, or 64 bit long, depending on the number of optional fields. Protobuf messages with more than 64 optional fields are therefore not supported. ##### Repeated fields Repeated fields in Protobuf messages are mapped to array fields in ROS 2 messages. This applies to all field types except to `bytes` fields. This exception is necessary as scalar `bytes` fields are already mapped to array fields in ROS 2. In this case, scalar type mapping rules are overridden and repeated `bytes` fields are mapped to array fields of `proto2ros/Bytes` ROS 2 message type. A sample equivalence is shown below.
Protobuf .proto definition ROS 2 .msg definition
```proto // some.proto message Payload { repeated int32 keys = 1; repeated bytes blobs = 2; bytes checksum = 3; } ``` ```python # Payload.msg int32[] keys proto2ros/Bytes[] blobs uint8[] checksum ```
##### One-of fields As ROS 2 messages lack the notion of one-of fields entirely, a ROS 2 message is generated for each one-of construct in a Protobuf message, bearing all one-of fields, as well as an integer `which` field. This ROS 2 message is functionally equivalent to a [tagged union](https://en.wikipedia.org/wiki/Tagged_union). For each field `f` in the one-of construct `o`, an integer constant `O_F_SET` tag is defined. Assigning the `which` field to a given tag thus conveys presence of the corresponding field. In place for each one-of construct, a message field of the corresponding type is defined. A sample equivalence is shown below.
Protobuf .proto definition ROS 2 .msg definition
```proto // some.proto message Timestamp { oneof value { uint64 seconds_since_epoch = 1; string datestring = 2; } } ``` ```python # Timestamp.msg /TimestampOneOfValue value ``` </td> </tr>
```python # TimestampSecondsSinceEpoch.msg uint64 seconds_since_epoch ```
```python # TimestampDatestring.msg string datestring ```
```python # TimestampOneOfValue.msg int8 VALUE_NOT_SET=0 int8 VALUE_SECOND_SINCE_EPOCH_SET=1 int8 VALUE_DATESTRING_SET=2 /TimestampSecondsSinceEpoch seconds_since_epoch /TimestampDatestring datestring int8 value_choice # deprecated int8 which ``` </td> </tr> </table> </div> **Implementation note**: 8 bit tags are used for one-of constructs. Protobuf messages with more than 256 one-of fields are therefore not supported. ##### Deprecated fields Deprecated fields are kept, unless `drop_deprecated` is enabled. If kept, these fields are annotated with a comment in the corresponding ROS 2 message definition. A sample equivalence is shown below.
Protobuf .proto definition ROS 2 .msg definition (drop_deprecated disabled) ROS 2 .msg definition (drop_deprecated enabled)
```proto // some.proto message Duration { int64 seconds = 1; int64 nanosec = 2 [deprecated = true]; int64 nanoseconds = 3; } ``` ```python # Duration.msg int64 seconds int64 nanosec # deprecated int64 nanoseconds ``` ```python # Duration.msg int64 seconds int64 nanoseconds ```
##### Reserved fields Reserved fields are ignored.
Protobuf .proto definition ROS 2 .msg definition
```proto // some.proto message Goal { string location = 1; reserved "time_budget"; } ``` ```python # Goal.msg string location ```
### Code generation To simplify conversion from Protobuf messages to equivalent ROS 2 messages and back, `proto2ros` generates conversion code, nicely wrapped around `convert(from, to)` function overloads (i.e. type dispatched). Note, however, that conversion code is only generated for message equivalences that `proto2ros` itself generated in full. For ad-hoc equivalences, as specified using message mappings, the user must implement the corresponding overloads. For auxiliary messages underpinning enums, map types, one-of fields, and the like, no overloads are generated at all (as there is no Protobuf message to convert to/from). #### Python APIs Conversion APIs in Python are exposed on a package basis, as `{ros_package_name}.conversions.convert` overloads. For each pair of equivalent `ROSMessageT` and `ProtoMessageT` types, `proto2ros` generates the following overloads: - `convert(ros_msg: ROSMessageT, proto_msg: ProtoMessageT) -> None` for ROS 2 message to Protobuf message conversion - `convert(proto_msg: ProtoMessageT, ros_msg: ROSMessageT) -> None` for Protobuf message to ROS 2 message conversion While convenient, [the mechanisms](https://pypi.org/project/multipledispatch) that enable these overloads do not play along with static analyzers such as `mypy`. To workaround this limitation, each overload is also made available, fully type annotated, under a unique name. This name is derived from argument type names as follows: - `convert_{ros_package_name}_{ros_message_name}_message_to_{proto_package_name}_{proto_message_name}_proto` for ROS 2 message to Protobuf message conversion - `convert_{proto_package_name}_{proto_message_name}_proto_to_{ros_package_name}_{ros_message_name}_message` for Protobuf message to ROS 2 message conversion All message names above are [snake-cased](https://en.wikipedia.org/wiki/Snake_case). Note that user-defined overloads for ad-hoc equivalences must follow the same pattern. **Implementation note**: all explicit and implicit `_pb2` (i.e. Protobuf) Python imports must be available at generation time. This requirement allows `proto2ros` to cope with an omission in Protobuf descriptor sets: these do not specify the mapping between fully qualified Protobuf message names and their Python counterparts. To workaround this limitation, known `_pb2` modules are traversed to reconstruct this mapping. #### C++ APIs Conversion APIs in C++ are exposed on a package basis as `{ros_package_name}::conversions::convert` overloads, available from `{ros_package_name}/conversions.hpp` headers. For each pair of equivalent `ROSMessageT` and `ProtoMessageT` types, `proto2ros` generates the following overloads: - `void {ros_package_name}::conversions::Convert(const ROSMessageT& ros_msg, ProtoMessageT* proto_msg)` for ROS 2 message to Protobuf message conversion - `void {ros_package_name}::conversions::Convert(const ProtoMessageT& proto_msg, ROSMessageT* ros_msg)` for Protobuf message to ROS 2 message conversion Note that user-defined overloads for ad-hoc equivalences must follow the same pattern. ## Configuration Both message and code generation are configured by a number of settings, listed below. | Name | Description | Default value | |---|---|---| | `drop_deprecated` | Whether to drop deprecated fields on conversion or not. If not dropped, deprecated fields are annotated with a comment. | `False` | | `passthrough_unknown` | Whether to forward Protobuf messages for which no equivalent ROS message is known as a serialized binary blob in a `proto2ros/AnyProto` field or not. | `True` | | `message_mapping` | A mapping from fully qualified Protobuf message names to fully qualified ROS message names. This mapping comes first during composite type translation. | `{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}` | | `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 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 dynamically typed, `proto2ros/Any` field. For further reference on any expansions, see `Any types` section below. | `True` | | `known_message_specifications` | A mapping from ROS message names to known message specifications. Necessary to cascade message generation for interdependent packages. | `{}` | | `cpp_headers` | Set of C++ headers to be included (as ``#include <{header}>``) in generated C++ conversion headers. Typically, Protobuf and ROS message C++ headers. | `[]` | | `inline_cpp_namespaces` | Set of C++ namespaces bearing conversion overloads, for which unqualified lookup (``using {namespace}::Convert``) is necessary in generated C++ conversion sources. | `[]` | | `python_imports` | Set of Python modules to be imported (as `import `) in generated conversion modules. Typically, Protobuf and ROS message Python modules. | `[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` | Set of Python modules to be imported into module scope (as `from import *`) in generated conversion modules. Typically, conversion Python modules. | `[proto2ros.conversions.basic]` | | `skip_implicit_imports` | Whether to skip importing Python modules for known Protobuf and ROS packages in generated conversion modules or not. These known modules are those derived from `.proto` source file names and the one homonymous to the ROS 2 package that hosts the generated interfaces. | `False` | These defaults can be replaced entirely via configuration file or overridden one by one via _configuration overlays_. Configuration overlays are configuration files that update the baseline configuration, default or user-defined. Scalar values are replaced, lists are extended, dictionaries are updated (i.e. shallow merged). ## Use cases ### Dual Protobuf / ROS 2 package A package may provide both Protobuf and ROS 2 messages, all generated from Protobuf definitions. ```cmake cmake_minimum_required(VERSION 3.12) project(proto2ros_tests) find_package(ament_cmake REQUIRED) find_package(builtin_interfaces REQUIRED) find_package(rosidl_default_generators REQUIRED) find_package(proto2ros REQUIRED) find_package(rclcpp REQUIRED) find_package(Protobuf REQUIRED) # Generate Python code for some.proto protobuf_generate( LANGUAGE python OUT_VAR proto_py_sources PROTOS some.proto IMPORT_DIRS proto ) # Generate C++ code for some.proto protobuf_generate( LANGUAGE cpp OUT_VAR proto_cpp_sources PROTOS some.proto IMPORT_DIRS proto ) # Build generated C++ code add_library(${PROJECT_NAME}_proto SHARED ${proto_cpp_sources}) target_include_directories(${PROJECT_NAME}_proto PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>" "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>" ) target_link_libraries(${PROJECT_NAME}_proto protobuf::libprotobuf) # Add dependable target for generated Protobuf code add_custom_target( ${PROJECT_NAME}_proto_gen ALL DEPENDS ${proto_py_sources} ${proto_cpp_sources} ) # Generate equivalent ROS 2 messages and conversion sources proto2ros_generate( ${PROJECT_NAME}_messages_gen PROTOS proto/some.proto INTERFACES_OUT_VAR ros_messages PYTHON_OUT_VAR ros_py_sources CPP_OUT_VAR cpp_sources INCLUDE_OUT_VAR cpp_include_dir APPEND_PYTHONPATH "${CMAKE_CURRENT_BINARY_DIR}" DEPENDS ${PROJECT_NAME}_proto_gen ) # Generate ROS 2 message code. rosidl_generate_interfaces( ${PROJECT_NAME} ${ros_messages} DEPENDENCIES builtin_interfaces proto2ros ) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}_messages_gen) # Build C++ conversion library add_library(${PROJECT_NAME}_conversions SHARED ${cpp_sources} src/manual_conversions.cpp) target_include_directories(${PROJECT_NAME}_conversions PUBLIC "$<BUILD_INTERFACE:${cpp_include_dir}>" "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>" ) 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 proto2ros rclcpp) # Install generated Python _pb2 and conversion code to the # Python package that is implicitly defined and installed by the # rosidl pipeline rosidl_generated_python_package_add( ${PROJECT_NAME}_additional_modules MODULES ${proto_py_sources} ${py_sources} PACKAGES ${PROJECT_NAME} DESTINATION ${PROJECT_NAME} ) # Install generated C++ .pb.h and conversion headers set(cpp_headers ${cpp_sources} ${proto_cpp_sources}) list(FILTER cpp_headers INCLUDE REGEX ".*\.hpp$") install( FILES ${cpp_headers} DESTINATION include/${PROJECT_NAME}/${PROJECT_NAME}/ ) # Install C++ Protobuf messages and conversion libraries install( TARGETS ${PROJECT_NAME}_proto ${PROJECT_NAME}_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}) ament_package() ``` [`proto2ros_tests`](https://github.com/bdaiinstitute/proto2ros/tree/main/proto2ros_tests) is a good example of this. ### ROS 2 vendored Protobuf messages Protobuf messages may already be provided by some third-party package, in which case, it is only the equivalent ROS 2 messages that are relevant. For a third-party package and `.proto` files that are hosted on public repositories, the [`FetchContent`](https://cmake.org/cmake/help/latest/module/FetchContent.html) module and the `proto2ros_vendor_package` CMake macro fully address this use case: ```cmake cmake_minimum_required(VERSION 3.8) project(vendored_third_party) find_package(ament_cmake REQUIRED) find_package(proto2ros REQUIRED) # Fetch third party package sources (incl. .proto files) include(FetchContent) FetchContent_Declare( third_party GIT_REPOSITORY ... GIT_TAG ..._ ) FetchContent_Populate(third_party) # Collect third party .proto files set(${PROJECT_NAME}_PROTO_DIR "${third_party_SOURCE_DIR}/protos") file(GLOB ${PROJECT_NAME}_PROTOS "${${PROJECT_NAME}_PROTO_DIR}/*.proto") # Generate ROS 2 messages and code (wraps rosidl) proto2ros_vendor_package(${PROJECT_NAME} PROTOS ${${PROJECT_NAME}_PROTOS} IMPORT_DIRS ${${PROJECT_NAME}_PROTO_DIR} ) ament_package() ``` [`bosdyn_msgs`](https://github.com/bdaiinstitute/bosdyn_msgs) is a good example of this.
CHANGELOG

Changelog for package proto2ros

1.0.0 (2024-12-02)

  • Add proto2ros documentation (#5)
  • Add GHA CI workflow (#3)
  • Please pre-commit linters (#2)
  • Bring sources from [bdaiinstitute/ros_utilities]{.title-ref} (#1)
  • Contributors: mhidalgo-bdai

Wiki Tutorials

This package does not provide any links to tutorials in it's rosindex metadata. You can check on the ROS Wiki Tutorials page for the package.

Launch files

No launch files found

Services

No service files found

Plugins

No plugins found.

Recent questions tagged proto2ros at Robotics Stack Exchange

No version for distro jazzy. Known supported distros are highlighted in the buttons above.
No version for distro rolling. Known supported distros are highlighted in the buttons above.
No version for distro noetic. Known supported distros are highlighted in the buttons above.
No version for distro ardent. Known supported distros are highlighted in the buttons above.
No version for distro bouncy. Known supported distros are highlighted in the buttons above.
No version for distro crystal. Known supported distros are highlighted in the buttons above.
No version for distro eloquent. Known supported distros are highlighted in the buttons above.
No version for distro dashing. Known supported distros are highlighted in the buttons above.
No version for distro galactic. Known supported distros are highlighted in the buttons above.
No version for distro foxy. Known supported distros are highlighted in the buttons above.
No version for distro iron. Known supported distros are highlighted in the buttons above.
No version for distro lunar. Known supported distros are highlighted in the buttons above.
No version for distro jade. Known supported distros are highlighted in the buttons above.
No version for distro indigo. Known supported distros are highlighted in the buttons above.
No version for distro hydro. Known supported distros are highlighted in the buttons above.
No version for distro kinetic. Known supported distros are highlighted in the buttons above.
No version for distro melodic. Known supported distros are highlighted in the buttons above.