From 6b68bfb2c05c065deaa9eccc297c7ee966d8066b Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 22 Apr 2024 12:00:50 +0200 Subject: [PATCH] Added ROS2-like Rust Examples (#104) --- examples/python/README.md | 16 +- .../python/{ => src}/add_two_ints_client.py | 2 +- .../{ => src}/fibonnacci_action_client.py | 5 +- examples/python/{ => src}/listener.py | 0 examples/python/{ => src}/talker.py | 0 examples/rust/Cargo.toml | 17 ++ examples/rust/README.md | 40 +++++ examples/rust/rust-toolchain.toml | 2 + examples/rust/src/bin/add_two_ints_client.rs | 75 +++++++++ .../rust/src/bin/fibonnacci_action_client.rs | 150 ++++++++++++++++++ examples/rust/src/bin/listener.rs | 62 ++++++++ examples/rust/src/bin/talker.rs | 64 ++++++++ 12 files changed, 422 insertions(+), 11 deletions(-) rename examples/python/{ => src}/add_two_ints_client.py (96%) rename examples/python/{ => src}/fibonnacci_action_client.py (95%) rename examples/python/{ => src}/listener.py (100%) rename examples/python/{ => src}/talker.py (100%) create mode 100644 examples/rust/Cargo.toml create mode 100644 examples/rust/README.md create mode 100644 examples/rust/rust-toolchain.toml create mode 100644 examples/rust/src/bin/add_two_ints_client.rs create mode 100644 examples/rust/src/bin/fibonnacci_action_client.rs create mode 100644 examples/rust/src/bin/listener.rs create mode 100644 examples/rust/src/bin/talker.rs diff --git a/examples/python/README.md b/examples/python/README.md index 628cdbf..af497e3 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -1,30 +1,30 @@ # Examples of Zenoh Python applications communicating with ROS 2 Nodes -## Messages Publication: [talker.py](talker.py) +## Messages Publication: [talker.py](src/talker.py) This code mimics the ROS 2 [Topics "talker" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/talker.cpp). It's compatible with the ROS 2 [Topics "listener" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/listener.cpp) running those commands: - `ros2 run demo_nodes_cpp listener` -- `zenho-bridge-ros2dds` +- `zenoh-bridge-ros2dds` - `python ./talker.py` -## Messages Subscription: [listener.py](listener.py) +## Messages Subscription: [listener.py](src/listener.py) This code mimics the ROS 2 [Topics "listener" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/listener.cpp). It's compatible with the ROS 2 [Topics "talker" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/talker.cpp) running those commands: - `ros2 run demo_nodes_cpp talker` -- `zenho-bridge-ros2dds` +- `zenoh-bridge-ros2dds` - `python ./listener.py` -## Services Client: [add_two_ints_client.py](add_two_ints_client.py) +## Services Client: [add_two_ints_client.py](src/add_two_ints_client.py) This code mimics the ROS 2 [Services "add_two_ints_client" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/services/add_two_ints_client.cpp). It's compatible with the ROS 2 [Services "add_two_ints_server" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/services/add_two_ints_server.cpp) running those commands: - `ros2 run demo_nodes_cpp add_two_ints_server` -- `zenho-bridge-ros2dds` +- `zenoh-bridge-ros2dds` - `python ./add_two_ints_client.py` -## Actions Client: [fibonnacci_action_client.py](fibonnacci_action_client.py) +## Actions Client: [fibonnacci_action_client.py](src/fibonnacci_action_client.py) This code mimics the ROS 2 [Actions "fibonnacci_action_client" demo](https://github.com/ros2/demos/blob/rolling/action_tutorials/action_tutorials_cpp/src/fibonacci_action_client.cpp). It's compatible with the ROS 2 [Actions "fibonnacci_action_server" demo](https://github.com/ros2/demos/blob/rolling/action_tutorials/action_tutorials_cpp/src/fibonacci_action_server.cpp) running those commands: - `ros2 run action_tutorials_cpp fibonacci_action_server` -- `zenho-bridge-ros2dds` +- `zenoh-bridge-ros2dds` - `python ./fibonnacci_action_client.py` diff --git a/examples/python/add_two_ints_client.py b/examples/python/src/add_two_ints_client.py similarity index 96% rename from examples/python/add_two_ints_client.py rename to examples/python/src/add_two_ints_client.py index f25e83d..857d728 100644 --- a/examples/python/add_two_ints_client.py +++ b/examples/python/src/add_two_ints_client.py @@ -28,7 +28,7 @@ class AddTwoInts_Request(IdlStruct, typename="AddTwoInts_Request"): # Equivalent to AddTwoInts.Response class, but serializable by pycdr2 @dataclass -class AddTwoInts_Response(IdlStruct, typename="AddTwoInts_Request"): +class AddTwoInts_Response(IdlStruct, typename="AddTwoInts_Response"): sum: pycdr2.types.int64 def main(): diff --git a/examples/python/fibonnacci_action_client.py b/examples/python/src/fibonnacci_action_client.py similarity index 95% rename from examples/python/fibonnacci_action_client.py rename to examples/python/src/fibonnacci_action_client.py index 9a04791..b7483e1 100644 --- a/examples/python/fibonnacci_action_client.py +++ b/examples/python/src/fibonnacci_action_client.py @@ -60,7 +60,7 @@ class Fibonacci_Feedback(IdlStruct, typename="Fibonacci_Feedback"): def feedback_callback(sample: zenoh.Sample): # Deserialize the message feedback = Fibonacci_Feedback.deserialize(sample.payload) - print('Received feedback: {0}'.format(feedback.partial_sequence)) + print('Next number in sequence received: {0}'.format(feedback.partial_sequence)) def main(): @@ -84,6 +84,7 @@ def main(): goal_id = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] req = Fibonacci_SendGoal_Request(goal_id, order=10) # Send the query with the serialized request + print('Sending goal') replies = session.get('fibonacci/_action/send_goal', zenoh.Queue(), value=req.serialize()) # Zenoh could get several replies for a request (e.g. from several "Service Servers" using the same name) for reply in replies.receiver: @@ -93,7 +94,7 @@ def main(): print('Goal rejected :(') return - print('Goal accepted :)') + print('Goal accepted by server, waiting for result') req = Fibonacci_GetResult_Request(goal_id) # Send the query with the serialized request diff --git a/examples/python/listener.py b/examples/python/src/listener.py similarity index 100% rename from examples/python/listener.py rename to examples/python/src/listener.py diff --git a/examples/python/talker.py b/examples/python/src/talker.py similarity index 100% rename from examples/python/talker.py rename to examples/python/src/talker.py diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml new file mode 100644 index 0000000..beea59f --- /dev/null +++ b/examples/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rust_examples" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +[dependencies] +async-std = { version = "1.12.0" } +futures = { version = "0.3.28" } +zenoh = { version = "0.10.1-rc" } +clap = { version = "4.4.11", features = ["derive"] } +env_logger = { version = "0.10.0" } +serde = {version = "1" } +serde_derive = {version = "1"} +cdr = {version = "0.2.4"} +log = { version = "0.4.21"} \ No newline at end of file diff --git a/examples/rust/README.md b/examples/rust/README.md new file mode 100644 index 0000000..3b89630 --- /dev/null +++ b/examples/rust/README.md @@ -0,0 +1,40 @@ +# Examples of Zenoh Rust applications communicating with ROS 2 Nodes + +## Building the examples +In order to build the examples you will need to: +* [Install Rust and Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) +* [Clone the repository](https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds) + +Once this is done, compile by running the following: +``` +cd examples/rust +cargo build +``` + +## Messages Publication: [talker.rs](src/bin/talker.rs) + +This code mimics the ROS 2 [Topics "talker" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/talker.cpp). It's compatible with the ROS 2 [Topics "listener" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/listener.cpp) running those commands: +- `ros2 run demo_nodes_cpp listener` +- `zenoh-bridge-ros2dds` +- `cargo run --bin talker` + +## Messages Subscription: [listener.rs](src/bin/listener.rs) + +This code mimics the ROS 2 [Topics "listener" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/listener.cpp). It's compatible with the ROS 2 [Topics "talker" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/topics/talker.cpp) running those commands: +- `ros2 run demo_nodes_cpp talker` +- `zenoh-bridge-ros2dds` +- `cargo run --bin listener` + +## Services Client: [add_two_ints_client.rs](src/bin/add_two_ints_client.rs) + +This code mimics the ROS 2 [Services "add_two_ints_client" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/services/add_two_ints_client.cpp). It's compatible with the ROS 2 [Services "add_two_ints_server" demo](https://github.com/ros2/demos/blob/rolling/demo_nodes_cpp/src/services/add_two_ints_server.cpp) running those commands: +- `ros2 run demo_nodes_cpp add_two_ints_server` +- `zenoh-bridge-ros2dds` +- `cargo run --bin add_two_ints_client` + +## Actions Client: [fibonnacci_action_client.rs](src/bin/fibonnacci_action_client.rs) + +This code mimics the ROS 2 [Actions "fibonnacci_action_client" demo](https://github.com/ros2/demos/blob/rolling/action_tutorials/action_tutorials_cpp/src/fibonacci_action_client.cpp). It's compatible with the ROS 2 [Actions "fibonnacci_action_server" demo](https://github.com/ros2/demos/blob/rolling/action_tutorials/action_tutorials_cpp/src/fibonacci_action_server.cpp) running those commands: +- `ros2 run action_tutorials_cpp fibonacci_action_server` +- `zenoh-bridge-ros2dds` +- `cargo run --bin fibonnacci_action_client` \ No newline at end of file diff --git a/examples/rust/rust-toolchain.toml b/examples/rust/rust-toolchain.toml new file mode 100644 index 0000000..b7eadd6 --- /dev/null +++ b/examples/rust/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.72.0" \ No newline at end of file diff --git a/examples/rust/src/bin/add_two_ints_client.rs b/examples/rust/src/bin/add_two_ints_client.rs new file mode 100644 index 0000000..3e6e843 --- /dev/null +++ b/examples/rust/src/bin/add_two_ints_client.rs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use cdr::{CdrLe, Infinite}; +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use zenoh::config::Config; +use zenoh::prelude::r#async::*; + +#[derive(Serialize, PartialEq, Debug)] +struct AddTwoIntsRequest { + a: i64, + b: i64, +} + +#[derive(Deserialize, PartialEq, Debug)] +struct AddTwoIntsResponse { + sum: i64, +} + +#[async_std::main] +async fn main() { + env_logger::init(); + + let config = parse_args(); + + let session = zenoh::open(config).res().await.unwrap(); + + let req = AddTwoIntsRequest { a: 2, b: 3 }; + let buf = cdr::serialize::<_, _, CdrLe>(&req, Infinite).unwrap(); + let replies = session + .get("add_two_ints") + .with_value(buf) + .res() + .await + .unwrap(); + + while let Ok(reply) = replies.recv_async().await { + match cdr::deserialize_from::<_, AddTwoIntsResponse, _>( + reply.sample.unwrap().payload.reader(), + cdr::size::Infinite, + ) { + Ok(res) => { + println!("Result of add_two_ints: {}", res.sum); + } + Err(e) => log::warn!("Error decoding message: {}", e), + } + } +} + +fn parse_args() -> Config { + let args = App::new("zenoh sub example") + .arg(Arg::from_usage( + "-c, --config=[FILE] 'A configuration file.'", + )) + .get_matches(); + + let config = if let Some(conf_file) = args.value_of("config") { + Config::from_file(conf_file).unwrap() + } else { + Config::default() + }; + + config +} diff --git a/examples/rust/src/bin/fibonnacci_action_client.rs b/examples/rust/src/bin/fibonnacci_action_client.rs new file mode 100644 index 0000000..f96c01d --- /dev/null +++ b/examples/rust/src/bin/fibonnacci_action_client.rs @@ -0,0 +1,150 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use cdr::{CdrLe, Infinite}; +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use zenoh::config::Config; +use zenoh::prelude::r#async::*; + +#[derive(Deserialize, PartialEq, Debug)] +struct Time { + sec: u32, + nsec: u32, +} + +#[derive(Serialize, PartialEq, Debug)] +struct FibonacciSendGoalRequest { + goal_id: [u8; 16], + order: i32, +} + +#[derive(Deserialize, PartialEq, Debug)] +struct FibonacciSendGoalResponse { + accepted: bool, + stamp: Time, +} + +#[derive(Serialize, PartialEq, Debug)] +struct FibonacciGetResultRequest { + goal_id: [u8; 16], +} + +#[derive(Deserialize, PartialEq, Debug)] +struct FibonacciGetResultResponse { + status: i8, + sequence: Vec, +} + +#[derive(Deserialize, PartialEq, Debug)] +struct FibonacciFeedback { + goal_id: [u8; 16], + partial_sequence: Vec, +} + +#[async_std::main] +async fn main() { + env_logger::init(); + + let config = parse_args(); + + let session = zenoh::open(config).res().await.unwrap(); + + let _subscriber = session + .declare_subscriber("fibonacci/_action/feedback") + .callback(|sample| { + match cdr::deserialize_from::<_, FibonacciFeedback, _>( + sample.value.payload.reader(), + cdr::size::Infinite, + ) { + Ok(msg) => { + println!( + "Next number in sequence received: {:?}", + msg.partial_sequence + ); + } + Err(e) => log::warn!("Error decoding message: {}", e), + }; + }) + .res() + .await + .unwrap(); + + let goal_id: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + let req = FibonacciSendGoalRequest { + goal_id: goal_id, + order: 10, + }; + + let buf = cdr::serialize::<_, _, CdrLe>(&req, Infinite).unwrap(); + println!("Sending goal"); + let replies = session + .get("fibonacci/_action/send_goal") + .with_value(buf) + .res() + .await + .unwrap(); + + while let Ok(reply) = replies.recv_async().await { + match cdr::deserialize_from::<_, FibonacciSendGoalResponse, _>( + reply.sample.unwrap().payload.reader(), + cdr::size::Infinite, + ) { + Ok(res) => { + if res.accepted { + println!("Goal accepted by server, waiting for result"); + } else { + println!("Goal rejected :("); + return; + } + } + Err(e) => log::warn!("Error decoding message: {}", e), + } + } + + let req = FibonacciGetResultRequest { goal_id: goal_id }; + let buf = cdr::serialize::<_, _, CdrLe>(&req, Infinite).unwrap(); + let replies = session + .get("fibonacci/_action/get_result") + .with_value(buf) + .res() + .await + .unwrap(); + while let Ok(reply) = replies.recv_async().await { + match cdr::deserialize_from::<_, FibonacciGetResultResponse, _>( + reply.sample.unwrap().payload.reader(), + cdr::size::Infinite, + ) { + Ok(res) => { + println!("Result: {:?}", res.sequence); + } + Err(e) => log::warn!("Error decoding message: {}", e), + } + } +} + +fn parse_args() -> Config { + let args = App::new("zenoh sub example") + .arg(Arg::from_usage( + "-c, --config=[FILE] 'A configuration file.'", + )) + .get_matches(); + + let config = if let Some(conf_file) = args.value_of("config") { + Config::from_file(conf_file).unwrap() + } else { + Config::default() + }; + + config +} diff --git a/examples/rust/src/bin/listener.rs b/examples/rust/src/bin/listener.rs new file mode 100644 index 0000000..f400664 --- /dev/null +++ b/examples/rust/src/bin/listener.rs @@ -0,0 +1,62 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use clap::{App, Arg}; +use serde::Deserialize; +use zenoh::config::Config; +use zenoh::prelude::r#async::*; + +#[derive(Deserialize, PartialEq, Debug)] +struct Message { + data: String, +} +#[async_std::main] +async fn main() { + // Initiate logging + env_logger::init(); + + let config = parse_args(); + + println!("Opening session..."); + let session = zenoh::open(config).res().await.unwrap(); + + let subscriber = session.declare_subscriber("chatter").res().await.unwrap(); + + while let Ok(sample) = subscriber.recv_async().await { + match cdr::deserialize_from::<_, Message, _>( + sample.value.payload.reader(), + cdr::size::Infinite, + ) { + Ok(msg) => { + println!("I heard: [{}]", msg.data); + } + Err(e) => log::warn!("Error decoding message: {}", e), + } + } +} + +fn parse_args() -> Config { + let args = App::new("zenoh sub example") + .arg(Arg::from_usage( + "-c, --config=[FILE] 'A configuration file.'", + )) + .get_matches(); + + let config = if let Some(conf_file) = args.value_of("config") { + Config::from_file(conf_file).unwrap() + } else { + Config::default() + }; + + config +} diff --git a/examples/rust/src/bin/talker.rs b/examples/rust/src/bin/talker.rs new file mode 100644 index 0000000..492bd8d --- /dev/null +++ b/examples/rust/src/bin/talker.rs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use async_std::task::sleep; +use cdr::{CdrLe, Infinite}; +use clap::{App, Arg}; +use serde::Serialize; +use std::time::Duration; +use zenoh::config::Config; +use zenoh::prelude::r#async::*; + +#[derive(Serialize, PartialEq, Debug)] +struct Message { + data: String, +} + +#[async_std::main] +async fn main() { + // Initiate logging + env_logger::init(); + + let config = parse_args(); + + println!("Opening session..."); + let session = zenoh::open(config).res().await.unwrap(); + + let publisher = session.declare_publisher("chatter").res().await.unwrap(); + + for idx in 0..u32::MAX { + sleep(Duration::from_secs(1)).await; + let message = Message { + data: format!("Hello World:{idx:4}"), + }; + let buf = cdr::serialize::<_, _, CdrLe>(&message, Infinite).unwrap(); + println!("Publishing: '{}')...", message.data); + publisher.put(buf).res().await.unwrap(); + } +} + +fn parse_args() -> Config { + let args = App::new("zenoh talker example") + .arg(Arg::from_usage( + "-c, --config=[FILE] 'A configuration file.'", + )) + .get_matches(); + + let config = if let Some(conf_file) = args.value_of("config") { + Config::from_file(conf_file).unwrap() + } else { + Config::default() + }; + + config +}