|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- # Dora Node API for C++
-
- Dora supports nodes written in C++ through this API crate.
-
- ## Build
-
- - Clone the `dora` repository:
- ```bash
- > git clone https://github.com/dora-rs/dora.git
- > cd dora
- ```
- - Build the `dora-node-api-cxx` package:
- ```bash
- cargo build --package dora-node-api-cxx
- ```
- - This will result in `dora-node-api.h` and `dora-node-api.cc` files in the `target/cxxbridge/dora-node-api-cxx` directory.
- - Include the `dora-node-api.h` header file in your source file.
- - Add the `dora-node-api.cc` file to your compile and link steps.
-
- ### Build with ROS2 Bridge
-
- Dora features an experimental ROS2 Bridge that enables dora nodes to publish and subscribe to ROS2 topics.
- To enable the bridge, use these steps:
-
- - Clone the `dora` repository (see above).
- - Source the ROS2 setup files (see [ROS2 docs](https://docs.ros.org/en/rolling/Tutorials/Beginner-CLI-Tools/Configuring-ROS2-Environment.html#source-the-setup-files))
- - Optional: Source package-specific ROS2 setup files if you want to use custom package-specific ROS2 messages in the bridge (see [ROS2 docs](https://docs.ros.org/en/rolling/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.html#source-the-setup-file))
- - Build the `dora-node-api-cxx` package **with the `ros2-bridge` feature enabled**:
- ```bash
- cargo build --package dora-node-api-cxx --features ros2-bridge
- ```
- - In addition to the `dora-node-api.h` and `dora-node-api.cc` files, this will place a `dora-ros2-bindings.h` and a `dora-ros2-bindings.cc` file in the `target/cxxbridge/dora-node-api-cxx` directory.
- - Include both the `dora-node-api.h` and the `dora-ros2-bindings.h` header files in your source file.
- - Add the `dora-node-api.cc` and `dora-ros2-bindings.cc` files to your compile and link steps.
-
- ## Usage
-
- The `dora-node-api.h` header provides various functions to interact with Dora.
-
- ### Init Dora Node
-
- All nodes need to register themselves with Dora at startup.
- To do that, call the `init_dora_node()` function.
- The function returns a `DoraNode` instance, which gives access to dora events and enables sending Dora outputs.
-
- ```c++
- auto dora_node = init_dora_node();
- ```
-
- ### Receiving Events
-
- The `dora_node.events` field is a stream of incoming events.
- To wait for the next incoming event, call `dora_node.events->next()`:
-
- ```c++
- auto event = dora_node.events->next();
- ```
-
- The `next` function returns an opaque `DoraEvent` type, which cannot be inspected from C++ directly.
- Instead, use the following functions to read and destructure the event:
-
- - `event_type(event)` returns a `DoraEventType`, which describes the kind of event. For example, an event could be an input or a stop instruction.
- - when receiving a `DoraEventType::AllInputsClosed`, the node should exit and not call `next` anymore
- - Events of type `DoraEventType::Input` can be downcasted using `event_as_input`:
- ```c++
- auto input = event_as_input(std::move(event));
- ```
- The function returns a `DoraInput` instance, which has an `id` and `data` field.
- - The input `id` can be converted to a C++ string through `std::string(input.id)`.
- - The `data` of inputs is currently of type [`rust::Vec<uint8_t>`](https://cxx.rs/binding/vec.html). Use the provided methods for reading or converting the data.
- - **Note:** In the future, we plan to change the data type to the [Apache Arrow](https://arrow.apache.org/) data format to support typed inputs.
-
- ### Sending Outputs
-
- Nodes can send outputs using the `send_output` output function and the `dora_node.send_output` field.
- Note that all outputs need to be listed in the dataflow YAML declaration file, otherwise an error will occur.
-
- **Example:**
-
- ```c++
- // the data you want to send (NOTE: only byte vectors are supported right now)
- std::vector<uint8_t> out_vec{42};
- // create a Rust slice from the output vector
- rust::Slice<const uint8_t> out_slice{out_vec.data(), out_vec.size()};
- // send the slice as output
- auto result = send_output(dora_node.send_output, "output_id", out_slice);
-
- // check for errors
- auto error = std::string(result.error);
- if (!error.empty())
- {
- std::cerr << "Error: " << error << std::endl;
- return -1;
- }
- ```
-
- ## Using the ROS2 Bridge
-
- The `dora-ros2-bindings.h` contains function and struct definitions that allow interacting with ROS2 nodes.
- Currently, the bridge supports publishing and subscribing to ROS2 topics.
- In the future, we plan to support ROS2 services and ROS2 actions as well.
-
- ### Initializing the ROS2 Context
-
- The first step is to initialize a ROS2 context:
-
- ```c++
- auto ros2_context = init_ros2_context();
- ```
-
- ### Creating Nodes
-
- After initializing a ROS2 context, you can use it to create ROS2 nodes:
-
- ```c++
- auto node = ros2_context->new_node("/ros2_demo", "turtle_teleop");
- ```
-
- The first argument is the namespace of the node and the second argument is its name.
-
- ### Creating Topics
-
- After creating a node, you can use one of the `create_topic_<TYPE>` functions to create a topic on it.
- The `<TYPE>` describes the message type that will be sent on the topic.
- The Dora ROS2 bridge automatically creates `create_topic_<TYPE>` functions for all messages types found in the sourced ROS2 environment.
-
- ```c++
- auto vel_topic = node->create_topic_geometry_msgs_Twist("/turtle1", "cmd_vel", qos_default());
- ```
-
- The first argument is the namespace of the topic and the second argument is its name.
- The third argument is the QoS (quality of service) setting for the topic.
- It can be adjusted as desired, for example:
-
- ```c++
- auto qos = qos_default();
- qos.durability = Ros2Durability::Volatile;
- qos.liveliness = Ros2Liveliness::Automatic;
- auto vel_topic = node->create_topic_geometry_msgs_Twist("/turtle1", "cmd_vel", qos);
- ```
-
- ### Publish
-
- After creating a topic, it is possible to publish messages on it.
- First, create a publisher:
-
- ```c++
- auto vel_publisher = node->create_publisher(vel_topic, qos);
- ```
-
- The returned publisher is typed by the chosen topic.
- It will only accept messages of the topic's type, otherwise a compile error will occur.
-
- After creating a publisher, you can use the `publish` function to publish one or more messages.
- For example:
-
- ```c++
- geometry_msgs::Twist twist = {
- .linear = {.x = 1, .y = 0, .z = 0},
- .angular = {.x = 0, .y = 0, .z = 0.5}
- };
- vel_publisher->publish(twist);
- ```
-
- The `geometry_msgs::Twist` struct is automatically generated from the sourced ROS2 environment.
- Since the publisher is typed, its `publish` method only accepts `geometry_msgs::Twist` messages.
-
-
- ### Subscriptions
-
- Subscribing to a topic is possible through the `create_subscription` function on nodes:
-
- ```c++
- auto pose_topic = node->create_topic_turtlesim_Pose("/turtle1", "pose", qos_default());
- auto pose_subscription = node->create_subscription(pose_topic, qos_default(), event_stream);
- ```
-
- The `topic` is the topic you want to subscribe to, created using a `create_topic_<TYPE>` function.
- The second argument is the quality of service setting, which can be customized as described above.
-
- The third parameter is the event stream that the received messages should be merged into.
- Multiple subscriptions can be merged into the same event stream.
-
- #### Combined Event Streams
-
- Combined event streams enable the merging of multiple event streams into one.
- The combined stream will then deliver messages from all sources, in order of arrival.
-
- You can create such a event stream from Dora's event stream using the `dora_events_into_combined` function:
-
- ```c++
- auto event_stream = dora_events_into_combined(std::move(dora_node.events));
- ```
-
- Alternatively, if you don't want to use Dora, you can also create an empty event stream:
-
- ```c++
- auto event_stream = empty_combined_events();
- ```
-
- **Note:** You should only use `empty_combined_events` if you're running your executable independent of Dora.
- Ignoring the events from the `dora_node.events` channel can result in unintended behavior.
-
- #### Receiving Messages from Combined Event Stream
-
- The merged event stream will receive all incoming events of the node, including Dora events and ROS2 messages.
- To wait for the next incoming event, use its `next` method:
-
- ```c++
- auto event = event_stream.next();
- ```
-
- This returns a `event` instance of type `CombinedEvent`, which can be downcasted to Dora events or ROS2 messages.
- To handle an event, you should check it's type and then downcast it:
-
- - To check for a Dora event, you can use the `is_dora()` function. If it returns `true`, you can downcast the combined event to a Dora event using the `downcast_dora` function.
- - ROS2 subscriptions support a `matches` function to check whether a combined event is an instance of the respective ROS2 subscription. If it returns true, you can downcast the event to the respective ROS2 message struct using the subscription's `downcast` function.
-
- **Example:**
-
- ```c++
- if (event.is_dora())
- {
- auto dora_event = downcast_dora(std::move(event));
- // handle dora_event as described above
- auto ty = event_type(dora_event);
- if (ty == DoraEventType::Input)
- {
- auto input = event_as_input(std::move(dora_event));
- // etc
- }
- // .. else if
- }
- else if (pose_subscription->matches(event))
- {
- auto pose = pose_subscription->downcast(std::move(event));
- std::cout << "Received pose x:" << pose.x << ", y:" << pose.y << std::endl;
- }
- else
- {
- std::cout << "received unexpected event" << std::endl;
- }
- ```
-
- ### Constants
-
- Some ROS2 message definitions define constants, e.g. to specify the values of an enum-like integer field.
- The Dora ROS2 bridge exposes these constants in the generated bindings as functions.
-
- For example, the `STATUS_NO_FIX` constant of the [`NavSatStatus` message](https://docs.ros.org/en/jade/api/sensor_msgs/html/msg/NavSatStatus.html) can be accessed as follows:
-
- ```c++
- assert((sensor_msgs::const_NavSatStatus_STATUS_NO_FIX() == -1));
- ```
-
- (Note: Exposing them as C++ constants is not possible because it's [not supported by `cxx` yet](https://github.com/dtolnay/cxx/issues/1051).)
-
- ### Service Clients
-
- To create a service client, use one of the `create_client_<TYPE>` functions.
- The `<TYPE>` describes the service type, which specifies the request and response types.
- The Dora ROS2 bridge automatically creates `create_client_<TYPE>` functions for all service types found in the sourced ROS2 environment.
-
- ```c++
- auto add_two_ints = node->create_client_example_interfaces_AddTwoInts(
- "/",
- "add_two_ints",
- qos,
- merged_events
- );
- ```
-
- - The first argument is the namespace of the service and the second argument is its name.
- - The third argument is the QoS (quality of service) setting for the service.
- It can be set to `qos_default()` or adjusted as desired, for example:
- ```c++
- auto qos = qos_default();
- qos.reliable = true;
- qos.max_blocking_time = 0.1;
- qos.keep_last = 1;
- ```
- - The last argument is the [combined event stream](#combined-event-streams) that the received service responses should be merged into.
-
- #### Waiting for the Service
-
- In order to achieve reliable service communication, it is recommended to wait until the service is available before sending requests.
- Use the `wait_for_service()` method for that, e.g.:
-
- ```c++
- add_two_ints->wait_for_service(node)
- ```
-
- The given `node` must be the node on which the service was created.
-
- #### Sending Requests
-
- To send a request, use the `send_request` method:
-
- ```c++
- add_two_ints->send_request(request);
- ```
-
- The method sends the request asynchronously without waiting for a response.
- When the response is received, it is automatically sent to the [combined event stream](#combined-event-streams) that was given on client creation.
-
- #### Receiving Responses
-
- See the [_"Receiving Messages from Combined Event Stream"_](#receiving-messages-from-combined-event-stream) section for how to receive events from the combined event stream.
- To check if a received event is a service response, use the `matches` method.
- If it returns `true`, you can use the `downcast` method to convert the event to the correct service response type.
-
- Example:
-
- ```c++
- if (add_two_ints->matches(event))
- {
- auto response = add_two_ints->downcast(std::move(event));
- std::cout << "Received sum response with value " << response.sum << std::endl;
- ...
- }
- ```
|