Skip to content

Commit 913fbe3

Browse files
esteveroboticswithjulia
authored andcommitted
build: reference example in tutorial
Signed-off-by: Esteve Fernandez <esteve@apache.org>
1 parent 4569882 commit 913fbe3

File tree

3 files changed

+203
-233
lines changed

3 files changed

+203
-233
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,5 @@ ros2 launch examples_rclrs_minimal_pub_sub minimal_pub_sub.launch.xml
7171
```
7272

7373
Further documentation articles:
74-
- [Tutorial on writing your first node with `rclrs`](./examples/minimal_pub_sub/src/first_rclrs_node.rs)
74+
- [Tutorial on writing your first node with `rclrs`](docs/writing-your-first-rclrs-node.md)
7575
- [Contributor's guide](docs/CONTRIBUTING.md)

docs/writing-your-first-rclrs-node.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Writing your first `rclrs` node
2+
3+
This tutorial is intended to point out some of the differences of the Rust client library with the other client libraries. It assumes knowledge of Rust, and is also not intended to be an introduction to ROS 2.
4+
5+
As a semi-realistic example, let's create a node that periodically republishes the last message it received. It's limited to only one specific message type – `std_msgs/msg/String` in this example.
6+
7+
## Create a package
8+
9+
In ROS 2, `ros2 pkg create` is the standard way of creating packages. However, the functionality to create Rust packages with this tool is not yet implemented, so they need to be created manually.
10+
11+
You can start by creating a package with `cargo` in the usual way:
12+
13+
```console
14+
cargo new republisher_node && cd republisher_node
15+
```
16+
17+
In the `Cargo.toml` file, add a dependency on `rclrs = "*"` and `std_msgs = "*"`.
18+
19+
Additionally, create a new `package.xml` if you want your node to be buildable with `colcon`. Make sure to change the build type to `ament_cargo` and to include the two packages mentioned above in the dependencies, as such:
20+
21+
```xml
22+
<package format="3">
23+
<name>republisher_node</name>
24+
<version>0.0.0</version>
25+
<description>TODO: Package description</description>
26+
<maintainer email="user@todo.todo">user</maintainer>
27+
<license>TODO: License declaration</license>
28+
29+
<depend>rclrs</depend>
30+
<depend>std_msgs</depend>
31+
32+
<export>
33+
<build_type>ament_cargo</build_type>
34+
</export>
35+
</package>
36+
```
37+
38+
39+
## Writing the basic node structure
40+
41+
Since Rust doesn't have inheritance, it's not possible to inherit from `Node` as is common practice in `rclcpp` or `rclpy`.
42+
43+
Instead, you can store the node as a regular member. Let's add a struct that contains the node, a subscription, and a field for the last message that was received to `main.rs`:
44+
45+
```rust
46+
use std::sync::Arc;
47+
use std_msgs::msg::String as StringMsg;
48+
49+
struct RepublisherNode {
50+
node: Arc<rclrs::Node>,
51+
_subscription: Arc<rclrs::Subscription<StringMsg>>,
52+
data: Option<StringMsg>,
53+
}
54+
55+
impl RepublisherNode {
56+
fn new(context: &rclrs::Context) -> Result<Self, rclrs::RclrsError> {
57+
let node = rclrs::Node::new(context, "republisher")?;
58+
let data = None;
59+
let _subscription = node.create_subscription(
60+
"in_topic",
61+
|msg: StringMsg| { todo!("Assign msg to self.data") },
62+
)?;
63+
Ok(Self {
64+
node,
65+
_subscription,
66+
data,
67+
})
68+
}
69+
}
70+
```
71+
72+
Next, add a main function to launch it:
73+
74+
```rust
75+
fn main() -> Result<(), rclrs::RclrsError> {
76+
let context = Context::default_from_env()?;
77+
let mut executor = context.create_basic_executor();
78+
let _republisher = RepublisherNode::new(&executor)?;
79+
executor
80+
.spin(SpinOptions::default())
81+
.first_error()
82+
.map_err(|err| err.into())
83+
}
84+
```
85+
86+
You should now be able to run this node with `cargo run`. However, the subscription callback still has a `todo!` in it, so it will exit with an error when it receives a message.
87+
88+
89+
## Storing received data in the struct
90+
91+
Let's do something about that `todo!`. The obvious thing for the subscription callback to do would be this:
92+
93+
```rust
94+
|msg: StringMsg| {
95+
data = Some(msg);
96+
},
97+
```
98+
99+
This is a standard pattern in C++, but doesn't work in Rust. Why not?
100+
101+
Written like this, `data` is *borrowed* by the callback, but `data` is a local variable which only exists in its current form until the end of `RepublisherNode::new()`. The subscription callback is required to not borrow any variables because the subscription, and therefore the callback, could live indefinitely.
102+
103+
> 💡 As an aside, this requirement is expressed by the `'static` bound on the generic parameter `F` for the callback in `Node::create_subscription()`.
104+
105+
You might think "I don't want to borrow from the local variable `data` anyway, I want to borrow from the `data` field in `RepublisherNode`!" and you would be right. That variable lives considerably longer, but also not forever, so it wouldn't be enough. A secondary problem is that it would be a *self-referential struct*, which is not allowed in Rust.
106+
107+
The solution is _shared ownership_ of the data by the callback and the node. The `Arc` type provides shared ownership, but since it only gives out shared references to its data, we also need a `Mutex` or a `RefCell`. This `Arc<Mutex<T>>` type is a frequent pattern in Rust code.
108+
109+
So, to store the received data in the struct, the following things have to change:
110+
1. Import `Mutex`
111+
2. Adjust the type of the `data` field
112+
3. Create two pointers to the same data (wrapped in a `Mutex`)
113+
4. Make the closure `move`, and inside it, lock the `Mutex` and store the message
114+
115+
```rust
116+
use rclrs::*;
117+
use std::sync::{Arc, Mutex}; // (1)
118+
use std_msgs::msg::String as StringMsg;
119+
struct RepublisherNode {
120+
_node: Arc<rclrs::Node>,
121+
_subscription: Arc<rclrs::Subscription<StringMsg>>,
122+
_data: Arc<Mutex<Option<StringMsg>>>, // (2)
123+
}
124+
impl RepublisherNode {
125+
fn new(executor: &rclrs::Executor) -> Result<Self, rclrs::RclrsError> {
126+
let _node = executor.create_node("republisher")?;
127+
let _data = Arc::new(Mutex::new(None)); // (3)
128+
let data_cb = Arc::clone(&_data);
129+
let _subscription = _node.create_subscription(
130+
"in_topic".keep_last(10).transient_local(), // (4)
131+
move |msg: StringMsg| {
132+
*data_cb.lock().unwrap() = Some(msg);
133+
},
134+
)?;
135+
Ok(Self {
136+
_node,
137+
_subscription,
138+
_data,
139+
})
140+
}
141+
}```
142+
143+
If that seems needlessly complicatedmaybe it is, in the sense that `rclrs` could potentially introduce new abstractions to improve the ergonomics of this use case. This is to be discussed.
144+
145+
If you couldn't follow the explanation involving borrowing, closures etc. above, an explanation of these concepts is unfortunately out of scope of this tutorial. There are many good Rust books and tutorials that can help you understand these crucial features. The online book [*The Rust Programming Language*](https://doc.rust-lang.org/book/) is a good place to start for most topics.
146+
147+
## Periodically run a republishing function
148+
149+
The node still doesn't republish the received messages. First, let's add a publisher to the node:
150+
151+
```rust:examples/minimal_pub_sub/src/first_rclrs_node.rs [5-10]
152+
```
153+
154+
Create a publisher and add it to the newly instantiated `RepublisherNode`:
155+
156+
```rust:examples/minimal_pub_sub/src/first_rclrs_node.rs [23-29]
157+
```
158+
159+
Then, let's add a `republish()` function to the `RepublisherNode` that publishes the latest message received, or does nothing if none was received:
160+
161+
```rust:examples/minimal_pub_sub/src/first_rclrs_node.rs [32-37]
162+
```
163+
164+
What's left to do is to call this function every second. `rclrs` doesn't yet have ROS timers, which run a function at a fixed interval, but it's easy enough to achieve with a thread, a loop, and the sleep function. Change your main function to spawn a separate thread:
165+
166+
```rust
167+
fn main() -> Result<(), rclrs::RclrsError> {
168+
let context = Context::default_from_env()?;
169+
let mut executor = context.create_basic_executor();
170+
let _republisher = RepublisherNode::new(&executor)?;
171+
std::thread::spawn(|| -> Result<(), rclrs::RclrsError> {
172+
loop {
173+
use std::time::Duration;
174+
std::thread::sleep(Duration::from_millis(1000));
175+
_republisher.republish()?;
176+
}
177+
});
178+
executor
179+
.spin(SpinOptions::default())
180+
.first_error()
181+
.map_err(|err| err.into())
182+
}
183+
```
184+
185+
But wait, this doesn't work – there is an error about the thread closure needing to outlive `'static`. That's again the same issue as above: Rust doesn't allow borrowing variables in this closure, because the function that the variable is coming from might return before the thread that borrows the variable ends.
186+
187+
> 💡 Of course, you could argue that this cannot really happen here, because returning from `main()` will also terminate the other threads, but Rust isn't that smart.
188+
189+
The solution is also the same as above: Shared ownership with `Arc`. Only this time, `Mutex` isn't needed since both the `rclcpp::spin()` and the `republish()` function only require a shared reference:
190+
191+
```rust:examples/minimal_pub_sub/src/first_rclrs_node.rs [40-55]
192+
```
193+
194+
195+
## Trying it out
196+
197+
In separate terminals, run `cargo run` and `ros2 topic echo /out_topic`. Nothing will be shown yet, since our node hasn't received any data yet.
198+
199+
In another terminal, publish a single message with `ros2 topic pub /in_topic std_msgs/msg/String '{data: "Bonjour"}' -1`. The terminal with `ros2 topic echo` should now receive a new `Bonjour` message every second.
200+
201+
Now publish another message, e.g. `ros2 topic pub /in_topic std_msgs/msg/String '{data: "Servus"}' -1` and observe the `ros2 topic echo` terminal receiving that message from that point forward.

0 commit comments

Comments
 (0)