Skip to content

Commit 5f2896f

Browse files
author
GueLaKais
committed
added more documentation to build your own ros2 rust nodes! yay
1 parent 22f3d81 commit 5f2896f

File tree

1 file changed

+271
-6
lines changed

1 file changed

+271
-6
lines changed

docs/writing_a_simple_publisher_and_subscriber.md

Lines changed: 271 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
* Time: 20 minutes
55
<details><summary>Background</summary>
66

7-
In this tutorial you will create
7+
In this tutorial you will create a
88
[nodes](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Nodes/Understanding-ROS2-Nodes.html) that pass information to each other via a
99
[topic](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Topics/Understanding-ROS2-Topics.html) in the form of string messages. The example used here is a simple "talker" and "listener" system; one node publishes data and the other subscribes to the topic to receive that data.
1010

11+
Since Rust doesn't have inheritance, it's not possible to inherit from `Node` as is common practice in [`rclcpp`](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Cpp-Publisher-And-Subscriber.html) or [`rclpy`](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html).
12+
1113
The code used in these examples can be found [here](https://gitlab.com/ros21923912/simple_ros2_node)
1214
<div style="margin-left:20px;">
1315
<details><summary>Sidenode to dependencies</summary>
@@ -31,8 +33,7 @@ Before developing ROS2 RUST nodes, you must follow the
3133
</details>
3234

3335
<details><summary>Tasks </summary>
34-
<div style="margin-left:20px;">
35-
<details><summary>Create a Package</summary>
36+
<div style="margin-left:20px;"><details><summary>Create a Package</summary>
3637

3738
Currently, building a package for ROS2 RUST is different
3839
from building packages for Python or C/C++.
@@ -42,7 +43,19 @@ project as follows:
4243
```
4344
cargo new your_project_name && your_project_name
4445
```
45-
In the [`Cargo.toml`](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html) file, add a dependency on `rclrs = "*"` and `std_msgs = "*"` by editing this file. For a full Introduction into RUST, please read the very good [RUST book](https://doc.rust-lang.org/book/title-page.html)
46+
In the [`Cargo.toml`](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html) file, add a dependency on `rclrs = "*"` and `std_msgs = "*"` by editing this file. For a full Introduction into RUST, please read the very good [RUST book](https://doc.rust-lang.org/book/title-page.html). Your `Cargo.toml` could now look like this:
47+
```
48+
[package]
49+
name = "your_package_name"
50+
version = "0.1.0"
51+
edition = "2021"
52+
53+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
54+
55+
[dependencies]
56+
rclrs = "*"
57+
std_msgs = "*"
58+
```
4659

4760

4861
Additionally, create a new `package.xml` if you want your node to be buildable with [`colcon`](https://colcon.readthedocs.io/en/released/user/installation.html). Make sure to change the build type to `ament_cargo` and to include the two packages mentioned above in the dependencies, as such:
@@ -62,9 +75,261 @@ Additionally, create a new `package.xml` if you want your node to be buildable w
6275
</export>
6376
</package>
6477
```
65-
<details><summary>Write the publisher node</summary><details>
78+
By taking a look at your package, for example by typing [`tree .`](https://www.geeksforgeeks.org/tree-command-unixlinux/) inside your package, and you'll see a structure similar to the following:
79+
```
80+
├── Cargo.toml
81+
├── package.xml
82+
└── src
83+
└── main.rs
6684
85+
2 directories, 3 files
86+
```
87+
Of course, you can use any capable editor or even your file explorer to do this.
6788

6889
</details>
69-
</div>
90+
91+
<details><summary>Write the publisher node</summary>
92+
93+
94+
95+
To construct a node, replace the code in your main.rs with the [following](https://gitlab.com/ros21923912/simple_ros2_node/-/raw/more_simple_nodes/src/simple_publisher.rs?ref_type=heads):
96+
```
97+
/// Creates a SimplePublisherNode, initializes a node and publisher, and provides
98+
/// methods to publish a simple "Hello World" message on a loop in separate threads.
99+
100+
/// Imports the Arc type from std::sync, used for thread-safe reference counting pointers,
101+
/// and the StringMsg message type from std_msgs for publishing string messages.
102+
use std::{sync::Arc,time::Duration,iter,thread};
103+
use rclrs::{RclrsError,QOS_PROFILE_DEFAULT,Context,create_node,Node,Publisher};
104+
use std_msgs::msg::String as StringMsg;
105+
// / SimplePublisherNode struct contains node and publisher members.
106+
// / Used to initialize a ROS 2 node and publisher, and publish messages.
107+
struct SimplePublisherNode {
108+
node: Arc<Node>,
109+
_publisher: Arc<Publisher<StringMsg>>,
110+
}
111+
/// Creates a new SimplePublisherNode by initializing a node and publisher.
112+
///
113+
/// The `new` function takes a context and returns a Result containing the
114+
/// initialized SimplePublisherNode or an error. It creates a node with the
115+
/// given name and creates a publisher on the "publish_hello" topic.
116+
///
117+
/// The SimplePublisherNode contains the node and publisher members.
118+
impl SimplePublisherNode {
119+
/// Creates a new SimplePublisherNode by initializing a node and publisher.
120+
///
121+
/// This function takes a context and returns a Result containing the
122+
/// initialized SimplePublisherNode or an error. It creates a node with the
123+
/// given name and creates a publisher on the "publish_hello" topic.
124+
///
125+
/// The SimplePublisherNode contains the node and publisher members.
126+
fn new(context: &Context) -> Result<Self,RclrsError> {
127+
let node = create_node(context, "simple_publisher").unwrap();
128+
let _publisher = node
129+
.create_publisher("publish_hello", QOS_PROFILE_DEFAULT)
130+
.unwrap();
131+
Ok(Self { node, _publisher, })
132+
}
133+
134+
/// Publishes a "Hello World" message on the publisher.
135+
///
136+
/// Creates a StringMsg with "Hello World" as the data, publishes it on
137+
/// the `_publisher`, and returns a Result. This allows regularly publishing
138+
/// a simple message on a loop.
139+
fn publish_data(&self,inkrement:i32) -> Result<i32,RclrsError> {
140+
141+
let msg: StringMsg = StringMsg {
142+
data: format!("Hello World {}",inkrement),
143+
};
144+
self._publisher.publish(msg).unwrap();
145+
Ok(inkrement+1_i32)
146+
}
147+
}
148+
149+
/// The main function initializes a ROS 2 context, node and publisher,
150+
/// spawns a thread to publish messages repeatedly, and spins the node
151+
/// to receive callbacks.
152+
///
153+
/// It creates a context, initializes a SimplePublisherNode which creates
154+
/// a node and publisher, clones the publisher to pass to the thread,
155+
/// spawns a thread to publish "Hello World" messages repeatedly, and
156+
/// calls spin() on the node to receive callbacks. This allows publishing
157+
/// messages asynchronously while spinning the node.
158+
fn main() -> Result<(),RclrsError> {
159+
let context = Context::new(std::env::args()).unwrap();
160+
let publisher = Arc::new(SimplePublisherNode::new(&context).unwrap());
161+
let publisher_other_thread = Arc::clone(&publisher);
162+
let mut iterator: i32=0;
163+
thread::spawn(move || -> () {
164+
iter::repeat(()).for_each(|()| {
165+
thread::sleep(Duration::from_millis(1000));
166+
iterator=publisher_other_thread.publish_data(iterator).unwrap();
167+
});
168+
});
169+
rclrs::spin(publisher.node.clone())
170+
}
171+
```
172+
173+
<details><summary>Examine the Code:</summary>
174+
175+
#### This first 3 lines of the Rust code imports tools for thread synchronization, time handling, iteration, threading, ROS 2 communication, and string message publishing. It's likely setting up a ROS 2 node that publishes string messages.
176+
```
177+
use std::{sync::Arc,time::Duration,iter,thread};
178+
use rclrs::{RclrsError,QOS_PROFILE_DEFAULT,Context,create_node,Node,Publisher};
179+
use std_msgs::msg::String as StringMsg;
180+
```
181+
* use std::{sync::Arc, time::Duration, iter, thread};: - Imports specific features from the standard library: - Arc is for thread-safe shared ownership of data. - Duration represents a time span. - iter provides tools for working with iterators. - thread enables creating and managing threads.
182+
* use rclrs::{RclrsError, QOS_PROFILE_DEFAULT, Context, create_node, Node, Publisher};: - Imports elements for ROS 2 communication: - RclrsError for handling errors. - QOS_PROFILE_DEFAULT likely for default Quality of Service settings. - Context, create_node, Node, Publisher are for ROS 2 node creation and publishing.
183+
* use std_msgs::msg::String as StringMsg;: - Imports the StringMsg type for publishing string messages.
184+
185+
#### Next this struct defines a SimplePublisherNode which holds references to a ROS 2 node and a publisher for string messages.
186+
```
187+
struct SimplePublisherNode {
188+
node: Arc<Node>,
189+
_publisher: Arc<Publisher<StringMsg>>,
190+
}
191+
```
192+
1. Structure:
193+
struct SimplePublisherNode: This line defines a new struct named SimplePublisherNode. It serves as a blueprint for creating objects that hold information related to a simple publisher node in ROS 2.
194+
195+
2. Members:
196+
* node: Arc<Node>: This member stores a reference to a ROS 2 node, wrapped in an Arc (Atomic Reference Counted) smart pointer. This allows for safe sharing of the node reference across multiple threads.
197+
* _publisher: Arc<Publisher<StringMsg>>: This member stores a reference to a publisher specifically for string messages (StringMsg), also wrapped in an Arc for thread safety. The publisher is responsible for sending string messages to other nodes in the ROS 2 system.
198+
199+
3. This code defines methods for the SimplePublisherNode struct. The new method creates a ROS 2 node and publisher, storing them in the struct. The publish_data method publishes a string message with a counter and returns the incremented counter.
200+
```
201+
impl SimplePublisherNode {
202+
fn new(context: &Context) -> Result<Self,RclrsError> {
203+
let node = create_node(context, "simple_publisher").unwrap();
204+
let _publisher = node
205+
.create_publisher("publish_hello", QOS_PROFILE_DEFAULT)
206+
.unwrap();
207+
Ok(Self { node, _publisher, })
208+
}
209+
fn publish_data(&self,inkrement:i32) -> Result<i32,RclrsError> {
210+
211+
let msg: StringMsg = StringMsg {
212+
data: format!("Hello World {}",inkrement),
213+
};
214+
self._publisher.publish(msg).unwrap();
215+
Ok(inkrement+1_i32)
216+
}
217+
}
218+
```
219+
220+
1. Implementation Block:
221+
`impl SimplePublisherNode { ... }`: This line indicates that methods are being defined for the `SimplePublisherNode` struct.
222+
223+
2. Constructor Method:
224+
* `fn new(context: &Context) -> Result<Self, RclrsError> { ... }`: This method serves as a constructor for creating instances of SimplePublisherNode.
225+
* It takes a Context object as input, which is necessary for interacting with the ROS 2 system.
226+
* It returns a Result type, indicating either a successful Self (the created SimplePublisherNode object) or an RclrsError if something goes wrong.
227+
* Inside the new method:
228+
* `let node = create_node(context, "simple_publisher").unwrap();`: Creates a new ROS 2 node named "simple_publisher" within the given context. The unwrap() unwraps the result, handling any errors immediately.
229+
* `let _publisher = node.create_publisher("publish_hello", QOS_PROFILE_DEFAULT).unwrap();`: Creates a publisher for string messages on the topic "publish_hello" with default quality of service settings.
230+
* `Ok(Self { node, _publisher, })`: Returns a Result with the newly created SimplePublisherNode object, containing the node and publisher references.
231+
232+
3. Publishing Method:
233+
* `fn publish_data(&self, inkrement: i32) -> Result<i32, RclrsError> { ... }`: This method publishes a string message and increments a counter.
234+
* It takes an inkrement value (an integer) as input, which is likely used for counting purposes within the message content.
235+
* It also returns a Result type, indicating either the incremented inkrement value or an RclrsError if publishing fails.
236+
* Inside the publish_data method:
237+
* `let msg: StringMsg = StringMsg { data: format!("Hello World {}", inkrement), };`:tCreates a string message with the content "Hello World" followed by the inkrement value.
238+
* self._publisher.publish(msg).unwrap();: Publishes the created message onto the topic associated with the publisher.
239+
* Ok(inkrement + 1_i32): Returns a Result with the incremented inkrement value.
240+
241+
#### The main Method creates a ROS 2 node that publishes string messages at a rate of 1 Hz.
242+
243+
```
244+
fn main() -> Result<(),RclrsError> {
245+
let context = Context::new(std::env::args()).unwrap();
246+
let publisher = Arc::new(SimplePublisherNode::new(&context).unwrap());
247+
let publisher_other_thread = Arc::clone(&publisher);
248+
let mut iterator: i32=0;
249+
thread::spawn(move || -> () {
250+
iter::repeat(()).for_each(|()| {
251+
thread::sleep(Duration::from_millis(1000));
252+
iterator=publisher_other_thread.publish_data(iterator).unwrap();
253+
});
254+
});
255+
rclrs::spin(publisher.node.clone())
256+
}
257+
```
258+
259+
1. Main Function:
260+
* `fn main() -> Result<(), RclrsError> { ... }`: This defines the main entry point of the program. It returns a Result type, indicating either successful execution or an RclrsError.
261+
262+
2. Context and Node Setup:
263+
264+
* `let context = Context::new(std::env::args()).unwrap();`: Creates a ROS 2 context using command-line arguments.
265+
* `let publisher = Arc::new(SimplePublisherNode::new(&context).unwrap());`:
266+
* Creates an [Arc (atomic reference counted)](https://doc.rust-lang.org/std/sync/struct.Arc.html) pointer to a `SimplePublisherNode` object.
267+
* Calls the new method on SimplePublisherNode to construct the node and publisher within the context.
268+
269+
3. Thread and Iterator:
270+
* `let publisher_other_thread = Arc::clone(&publisher);`: Clones the shared publisher pointer for use in a separate thread.
271+
* `let mut iterator: i32 = 0;`: Initializes a counter variable for message content.
272+
* `thread::spawn(move || -> () { ... });`: Spawns a new thread with a closure: `iter::repeat(()).for_each(|()| { ... });`: Creates an infinite loop using `iter::repeat`.
273+
274+
4. Publishing Loop within Thread:
275+
276+
* `thread::sleep(Duration::from_millis(1000));`: Pauses the thread for 1 second (1 Hz publishing rate).
277+
* `iterator = publisher_other_thread.publish_data(iterator).unwrap();`: Calls the publish_data method on the publisher_other_thread to publish a message with the current counter value. Increments the iterator for the next message.
278+
279+
5. Main Thread Spin:
280+
* `rclrs::spin(publisher.node.clone());`: Keeps the main thread running, processing ROS 2 events and messages. Uses a cloned reference to the node to ensure it remains active even with other threads.
281+
282+
</details>
283+
284+
Once you have implemented the code, you are ready to run it:
285+
```
286+
cd ${MainFolderOfWorkspace}
287+
colcon build
288+
source install/setub.bash
289+
```
290+
And finally run with:
291+
```
292+
ros2 run your_project_name your_project_name
293+
```
294+
(Please give your package a better name than me ;) )
70295
</details>
296+
<details><summary>Having several Ros2 rust nodes in one Package</summary>
297+
298+
Of course, you can write for each node you want to implement its own package, and that can have it's advantages. I implore you to use some cargo tricks and add some binary targets to your `cargo.toml`. this could look like this:
299+
```
300+
[package]
301+
name = "your_package_name"
302+
version = "0.1.0"
303+
edition = "2021"
304+
305+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
306+
[[bin]]
307+
name="simple_publisher"
308+
path="src/main.rs"
309+
[dependencies]
310+
rclrs = "*"
311+
std_msgs = "*"
312+
```
313+
You'll find the name of your executable and the corresponding file name under the `[[bin]]` tag. As you can see, the filename and the name you want to call your node don't have to match. Please remember to include your executable name with snake_cases. The rust compiler will be a bit grumpy if you don't.
314+
Now, by recompiling the package from the previous chapter and making it usable:
315+
```
316+
cd ${MainFolderOfWorkspace}
317+
colcon build
318+
source install/setub.bash
319+
```
320+
node will look like this:
321+
```
322+
ros2 run your_package_name simple_publisher
323+
```
324+
As you can see, you are now calling your node by the name declared in `[[bin]]` using the `name` variable.
325+
</details>
326+
<details><summary>Write the subscriber node</summary>
327+
328+
Of course, you can implement a new ros2 rust package for this node. You can find out how to do this in the section called 'Create a package'.
329+
Or you can add a new binary target to your package. Then just add a new `<file>.rs` to your source directory - for simplicity I'll call this file `simple_subscriber.rs` - and add a corresponding binary target to your `Cargo.toml`:
330+
```
331+
332+
```
333+
334+
</details>
335+
</details></div>

0 commit comments

Comments
 (0)