Skip to content

feat: initial ROS2 AsyncAPI contribution by SIEMENS AG #270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This repository contains the specifications for each AsyncAPI protocol binding.
* [NATS binding](./nats)
* [Pulsar](./pulsar)
* [Redis binding](./redis)
* [ROS2](./ros2)
* [SNS binding](./sns)
* [Solace binding](./solace)
* [SQS binding](./sqs)
Expand Down
185 changes: 185 additions & 0 deletions ros2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# ROS 2 Bindings

This document defines how to describe ROS 2-specific information in AsyncAPI.

It applies to all versions of ROS 2 (foxy, galactic, humble, iron, jazzy).

<a name="version"></a>

## Version

Current version is `0.1.0`.

<a name="server"></a>

## Server Binding Object

This object contains information about the server representation in ROS 2.
ROS 2 can use either DDS or Zenoh as its middleware.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor perhaps, but while the main RMWs in use may be DDS-based, there are certainly others that aren't (rmw_zenoh being a recent addition, but also eclipse-ecal/rmw_ecal fi).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment!

As you know, there are a quite acouple of them with my favourite (for the sake of this exact argument) the christophebedard/rmw_email.

We also had this argument earlier with @fmvilas , that for the moment we only want to focus on DDS and Zenoh based systems. But I think that the majority of the RMW implementations should also be representable through this binding. But this current implication should be put out exactly in this sentence. Therefor, thank you for highlighting it!

Additionally, we are already indicating the vast options one can choose from with ROS2 here:

`rmwImplementation` | string | Specifies the ROS 2 middleware implementation to be used. Valid values include the different [ROS 2 middleware vendors (RMW)](https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Different-Middleware-Vendors.html) like `rmw_fastrtps_cpp` (Fast DDS) or `rmw_zenoh_cpp` (Zenoh). This determines the underlying middleware implementation that handles communication.

DDS is decentralized with no central server, so the field `host` can set to `localhost`.
When using Zenoh, the `host` field specifies the Zenoh Router IP address.

###### Fixed Fields

Field Name | Type | ROS 2 Versions | Description
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you're including a "ROS 2 Versions" column but this column always has the "all" value. Should we get rid of it or do you expect future properties to appear only for certain versions of ROS 2?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. We'll do that

Copy link
Author

@gramss gramss Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently this list is not comprehensive. This could be much larger but a better place to look for changes is here: e.g. for Jazzy: official ROS doc for release changelog

For an initial PR I would leave it that way and depending of the acceptance and usage of AsyncAPI in ROS this can be extended more and bring back the ROS 2 Version column that way.

Leaving this open to feedback from others for now, but the change is reflected in the latest commits

---|:---:|:---:|---|
`rmwImplementation` | string | all | Specifies the ROS 2 middleware implementation to be used. Valid values include `rmw_fastrtps_cpp` (Fast DDS), `rmw_cyclonedds_cpp` (Cyclone DDS), `rmw_connext_cpp` (RTI Connext), and `rmw_zenoh_cpp` (Zenoh). This determines the underlying middleware implementation that handles communication.
`domainId` | integer | all | All ROS 2 nodes use domain ID 0 by default. To prevent interference between different groups of computers running ROS 2 on the same network, a group can be set with a unique domain ID. Must be a non-negative integer less than 232.

### Examples

```yaml
servers:
ros2:
host: localhost
protocol: ros2
protocolVersion: humble
bindings:
ros2:
rmwImplementation: rmw_fastrtps_cpp
domainId: 0
```


<a name="channel"></a>

## Channel Binding Object

A channel represents a ROS 2 topic, service or action interface.

This object DOES NOT contain any ROS 2 specific properties.

Example - ROS 2 topic:

```yaml
address: /turtle1/cmd_vel
messages:
TwistMsg:
$ref: '#/components/messages/TwistMsg'
```
Example - ROS 2 action:

```yaml
RotateAbsoluteRequest:
address: /turtle1/rotate_absolute
messages:
RotateAbsoluteActionRequest:
$ref: '#/components/messages/RotateAbsoluteActionRequest'

RotateAbsoluteReply:
address: /turtle1/rotate_absolute
messages:
RotateAbsoluteActionResult:
$ref: '#/components/messages/RotateAbsoluteActionResult'
RotateAbsoluteActionFeedback:
$ref: '#/components/messages/RotateAbsoluteActionFeedback'
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no channel binding, I recommend leaving it out of the document. E.g., the way we're doing with the AMQP 1.0 channel binding: https://github.com/asyncapi/bindings/tree/master/amqp1#channel-binding-object.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. We'll do that

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm.. Just had time to dig a little bit deeper into channels.

Would you describe this picture here as a channel?
image
Source from OSRF

Or another example here (circle = node == application and box = topic == message)

The message /joint_states is consumed by 2 nodes..

For me, topic translates to an asyncAPI message and a ros node translates to an asyncAPI application.
There can be one-to-many communications in ROS between one node (pub) and many other nodes (subs). Same applies for the other ros2 roles.

But I am unsure how this can be represented as a channel here. The specifics how the message is transported to many clients is defined inside of the concrete rmw implementation(?). A node can only tune how to react to a new event but not influence the transport mode other than specifying the QoS level.

Does @Achllle maybe has an idea about that? Here is the asyncAPI definition: link

I am having a hard time especially with translating this sentence from the doc to ROS:

The producer sends a message through the channel, which then queues the message for delivery to the appropriate consumers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm 🤔 from what I'm reading here, it seems that AsyncAPI channels map to ROS2 topics and AsyncAPI messages map to ROS2 messages (or data types, data units). So, from what I'm learning so far:

  • ROS2 node -> AsyncAPI application (i.e., the whole AsyncAPI file)
  • ROS2 middleware (DDS, Zenoh) -> AsyncAPI server.
  • ROS2 topic -> AsyncAPI channel
  • ROS2 message/data type -> AsyncAPI message

Feel free to correct me as I'm looking at this for the first time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the docs here:

        self.subscription = self.create_subscription(
            String,
            'topic', # <-- This is the AsyncAPI's channel address
            self.listener_callback,
            10)
        self.subscription  # prevent unused variable warning

    def listener_callback(self, msg): # <-- `msg` is the AsyncAPI's message
        self.get_logger().info('I heard: "%s"' % msg.data) # <-- msg.data is the AsyncAPI's message payload

And I'm assuming, in listener_callback, the msg variable will contain a msg.header which maps directly to AsyncAPI's message headers.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gavanderhoorn thank you for the proper fact checking and updating (at least) me on the current state of some ROS developments. 👍

We can bring the topic about a ROS2 release-versioned database of common-msgs in the mentioned discourse discussion.

I am open to discuss this topic. The binding should include a guideline how to deal with common messages regardless of the outcome of this discussion.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A specific (recent) distribution of ROS 2 + the package name + interface name is the only thing needed to fully define a message. It feels somewhat silly to redefine such messages in the asyncAPI. Without the message headers or import it's not possible (AFAIK) to create the payload like one would do with a json blob. To me the ideal experience would involve linking to the interface definition rather than redefining it. When someone intends to update to a newer distribution, they would have to manually validate all AsyncAPI messages to match the new distribution. Most ROS applications heavily depend on those interface definitions included in ros-base or other common packages, so this would mean a lot of duplication and room for user error.

I don't really see the point with 'black box code'. Such repos will have at least their interface descriptions made available, so whether the code is accessible doesn't really matter.

Perhaps this 'linking' is possible through a binding where if it's a standard message, you'd link to it with:

$ref: https://docs.ros.org/en/$ROS_DISTRO/p/geometry_msgs/msg/Twist.html
# OR
$ref: https://github.com/ros2/common_interfaces/blob/$ROS_DISTRO/geometry_msgs/msg/Twist.msg
# OR
$ref: my_custom_interfaces/msg/MyMsg.msg

and we include some magic to render the definitions when they're found.

Looking at NoDL, it seems that this approach is happening there as well.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Achllle First, thank you for your thoughtful feedback on this discussion. I appreciate your expertise in ROS2 and would like to address some of the points you raised.

Regarding References in AsyncAPI

I understand your suggestion about using direct URL references to ROS2 message definitions. While this approach seems intuitive from a ROS2 perspective, the AsyncAPI specification has a specific way of defining message payloads, which relies on the $ref keyword to reference reusable schema definitions. From my understanding, it's not possible to use a direct URL in this context
The $ref syntax you proposed would require custom extensions or resolvers that aren't part of the standard AsyncAPI functionality. This could create compatibility issues with existing AsyncAPI tools and validators.

On Message Definition Duplication

You make an excellent point that a ROS2 distribution + package name + interface name is sufficient to fully define a message. What might seem like redundant redefinition actually serves an important purpose in our approach.
By including complete message definitions within the AsyncAPI document, we enable:

  • Automated Code Generation: Tools can generate client/server code, blocks for low code tools as behavior tree or node red, directly from the AsyncAPI document if it contains all the necessary information.
  • Proper HTML documentation: Having complete message definitions embedded in the AsyncAPI document ensures that the generated documentation is self-contained and fully informative, allowing even those unfamiliar with ROS2 to understand the API structure and message formats without needing to reference external resources.

Our Approach: Automation Over Manual Definition

I believe we might be aligned on the core issue but approaching it differently. Our goal isn't to have developers manually redefine ROS2 messages in AsyncAPI (which would indeed be tedious and error-prone). Instead, we envision automated tools that:

  1. Extract message definitions from ROS2 packages
  2. Convert them to AsyncAPI format
  3. Generate the complete AsyncAPI document with all necessary definitions included

This way, developers don't need to manually maintain these definitions - the tooling handles the synchronization between ROS2 interfaces and AsyncAPI documents

Copy link

@Achllle Achllle Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fmvilas can you please comment on

would require custom extensions or resolvers that aren't part of the standard AsyncAPI functionality.

specifically whether this would be achievable today through e.g. bindings? Is there any precedent for this as well, such as including existing json/yaml schemas that are defined outside AsyncAPI? Are those re-defined within the AsyncAPI spec?

I understand it may be harder to do this, but it's hard to argue that it's also not superior. Automated generation doesn't solve the issue of duplicating information, thus leading to potential conflicts, especially since messages can vary between ROS 2 distros.

generated documentation is self-contained and fully informative, allowing even those unfamiliar with ROS2 to understand the API structure and message formats without needing to reference external resources.

Documentation generation can take care of linking or even embedding the message definitions so that would address self-containment. That would also solves the issue for those unfamiliar with ROS 2. I'm also not sure why someone unfamiliar with ROS 2 would want to deeply interact with a ROS 2 API without first learning ROS 2 basics.

That being said, if it proves unattainable or would require updating the spec in a major way, I'm fine with the duplication approach, though I might consider using NoDL in that case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree on getting @fmvilas opinion on this.

I think we cannot alter it from this binding-spec but rather would need to make contributions to the larger asyncAPI body.

@Achllle we should also look at the problem from another angle. ROS already does a great job at defining its messages through a standard approach on the code level with the dedicated .msg / .srv and .action files. Are other supported protocols also doing that way? I was not able to gather such information about MQTT for instance.


Regarding your concerns:

issue of duplicating information, thus leading to potential conflicts, especially since messages can vary between ROS 2 distros

Can we dig deeper into this?
Are you concerned about the duplication itself or more about the missing reference to the original origin?
The datatypes need to be translated anyway from ROS into AsyncAPI at some point. Either at a rendering stage of HTML or before by holding the information in files.
We could influence this from the ROS UX so that those files are only generated on-demand and deleted afterwards.

If your only concern might be regarding different ros distros, this information is also available at the head asyncAPI file. But this is not pinned on the message level for mix-bistro-robots (that I would like to neglect if possible at a first stage?).

I'm also not sure why someone unfamiliar with ROS 2 would want to deeply interact with a ROS 2 API without first learning ROS 2 basics.

Reading a documentation of a robot and finding information (where and if e.g. a camera raw stream is made available) is a step before going into the details of a specific application. Could be that a robot offers such information only as a websocket, then one does not need to go deeper into the ROS universe.

Generally, I would love to bring this discussion on a use-case based requirement level. I totally respect your view on this subject but would love to bring it into use-case scenarios that we can either accept or reject (non-relevant) and with it the proposing requirements.
This is something we could focus on the meeting next week.


<a name="operation"></a>

## Operation Binding Object

AsyncAPI operations with their `send` and `receive` actions map directly to ROS 2 subscribers, publishers, actions or services.
- send -> `publisher`, `action_client`, `service_client`
- receive -> `subscriber`, `action_server`, `service_server`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this. Could we improve this description?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be put in the description field of the table

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I'm nitpicking, but technically speaking:

Suggested change
AsyncAPI operations with their `send` and `receive` actions map directly to ROS 2 subscribers, publishers, actions or services.
- send -> `publisher`, `action_client`, `service_client`
- receive -> `subscriber`, `action_server`, `service_server`
AsyncAPI operations with their `send` and `receive` actions map directly to ROS 2 subscribers, publishers, actions or services.
- producer -> `publisher`, `action_client`, `service_client`
- consumer -> `subscriber`, `action_server`, `service_server`

send and receive refers to publisher.publish(), receive would point to a callback()/register_callback()

Copy link

@amparo-siemens amparo-siemens Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why we used send/receive is because those are the only options for the field action in AsyncAPI and we were tying to map it to the different ROS2 types, since we cannot change the valid options for the field action


###### Fixed Fields

Field Name | Type | ROS 2 Versions | Description
---|:---:|:---:|---|
`type` | string | all | Specifies the ROS 2 type of the node for this operation. Valid values are: `publisher`, `subscriber`, `service_client`, `service_server`, `action_client`, `action_server`. This defines how the node will interact with the associated topic or action.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if I understand correctly, when the action field is send, this value can be publisher, action_client, or service_client. And when action is receive, this value can be subscriber, action_server, or service_server. Is that right? If so, this relationship should be expressed here instead of above.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we will put the explanation here

`node` | string | all | The name of the ROS 2 node that implements this operation.
`qosPolicies` | object | all | Quality of Service (QoS) for the topic.

### Quality of Service Object
This object contains ROS 2 specific information about the Quality of Service policies.
More information here: https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html#qos-policies

Field Name | Type | ROS 2 Versions | Description
---|:---:|:---:|---|
`reliability` | string | all | One of `best_effort` or `reliable`. More information here: [ROS 2 QoS](https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html#qos-policies)
`history` | string | all | One of `keep_last`, `keep_all` or `unknown`. More information here: [ROS 2 QoS](https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html#qos-policies)
`durability` | string | all | One of `transient_local` or `volatile`. More information here: [ROS 2 QoS](https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html#qos-policies)
`lifespan` | integer | all | The maximum amount of time between the publishing and the reception of a message without the message being considered stale or expired. `-1` means infinite.
`deadline` | integer | all | The expected maximum amount of time between subsequent messages being published to a topic. `-1` means infinite.
`liveliness` | string | all | One of `automatic`or `manual`. More information here: [ROS 2 QoS](https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html#qos-policies)
`leaseDuration` | integer | all | the maximum period of time a publisher has to indicate that it is alive before the system considers it to have lost liveliness. `-1` means infinite.
### Examples

ROS 2 subscriber example:

```yaml
receiveCmdVel:
action: receive
channel:
$ref: "#/channels/CmdVel"
bindings:
ros2:
role: subscriber
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to make this value the default so people don't have to specify it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion does not make sense since in ROS2 there is no default value for roles. It depends if you are using topics, actions, services... and it depends if you are publishing, subscribing... But we are completely open to suggestions. @Achllle what is your opinion here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not a strong opinion on this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I understand role. I can't find it in the adding bindings doc.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to have a ros2 binding named as "role" inside the operation bindings to define if a node is publisher/subscriber or service/action client/server. Right now you don't see it in the binding documentation because it does not exist yet. What we are trying here is to define the necessary bindings to put them as official bindings (in the same way that you see right know the mqtt ones) and then you will be able to see them. Right know all ros2 bindings that don't use the existing ones, are experimental.
Summarizing, in bindings we can get "anything" we want.

node: /turtlesim
qosPolicies:
history: unknown
reliability: reliable
durability: volatile
lifespan: -1
deadline: -1
liveliness: automatic
leaseDuration: -1
```
ROS 2 action Server example:

```yaml
receiveRotateAbsolute:
action: receive
channel:
$ref: "#/channels/RotateAbsoluteRequest"
reply:
channel:
$ref: "#/channels/RotateAbsoluteReply"
bindings:
ros2:
role: action_server
node: /turtlesim
```


<a name="message"></a>

## Message Binding Object

ROS 2 message types (defined in .msg/.srv/.aciton files) are mapped to AsyncAPI message payloads.

This object DOES NOT contain any ROS 2 specific properties.

```yaml
Vector3Msg:
type: object
properties:
x:
type: number
format: double
y:
type: number
format: double
z:
type: number
format: double
```

ROS 2 Type | AsyncAPI Type | AsyncAPI Format |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ROS 2 Type | AsyncAPI Type | AsyncAPI Format |
ROS 2 Type | AsyncAPI Type | AsyncAPI Format |

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this table about? When should it be used and what for?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we should delete the table from here since it is not about the bindings but we should have this table somewhere. We think it is important that ros developers have this table to know how to express the ros2 messages types in asyncAPI format.
@Achllle do you think that we should put this information in the ros2 documentation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'm all in with having it, I just think we have to explain what is for. Right now it's just there without any further explanation.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do believe this belongs here, agreed with adding explanation. Suggest including a link to the design document

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We included the design document suggested by @Achllle .

But @fmvilas question is not answered:

explain what [this table] is for

I think such an information could be needed to properly integrate/visualize different bindings in one tool. That is why you have your AsyncAPI message types and formats for.

Right now, this is used as our Rosetta Stone table for the siemens PoC AsyncAPI generator reading ros message file definitions and producing respecting AsyncAPI files.

I hope that the current wording reflects this already. If not, we can make it more distinctive.

---|:---:|---|
bool | boolean | boolean
byte | string | octet
char | integer | uint8
float32 | number | float
float64 | number | double
int8 | integer | int8
uint8 | integer | uint8
int16 | integer | int16
uint16 | integer | uint16
int32 | integer | int32
uint32 | integer | uint32
int64 | integer | int64
uint64 | integer | uint64
string | string | string
array | array | --
Loading