diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..26ecfe1 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +# Examples of Zenoh applications communicating with ROS 2 Nodes + +This directory contains some examples of applications using the Zenoh APIs only (not ROS 2), and that can communicate with ROS 2 Nodes using CycloneDDS as RMW, via the `zenoh-bridge-ros2dds`. + +> :warning: The code of those examples are made to work with the version 0.10.x of the `zenoh-plugin-ros2dds` or `zenoh-bridge-ros2dds`. Soon both will be adapted for compatibility with the incoming `rmw_zenoh`, implying some code change in those examples. diff --git a/examples/python/README.md b/examples/python/README.md new file mode 100644 index 0000000..628cdbf --- /dev/null +++ b/examples/python/README.md @@ -0,0 +1,30 @@ +# Examples of Zenoh Python applications communicating with ROS 2 Nodes + + +## Messages Publication: [talker.py](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` +- `python ./talker.py` + +## Messages Subscription: [listener.py](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` +- `python ./listener.py` + +## Services Client: [add_two_ints_client.py](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` +- `python ./add_two_ints_client.py` + +## Actions Client: [fibonnacci_action_client.py](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` +- `python ./fibonnacci_action_client.py` diff --git a/examples/python/add_two_ints_client.py b/examples/python/add_two_ints_client.py new file mode 100644 index 0000000..f25e83d --- /dev/null +++ b/examples/python/add_two_ints_client.py @@ -0,0 +1,62 @@ +# +# Copyright (c) 2024 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, +# +import time +import argparse +import zenoh + +# pycdr2 is the serializer of data in CDR format (required by ROS2) +import pycdr2 +from pycdr2 import IdlStruct +from dataclasses import dataclass + +# Equivalent to AddTwoInts.Request class, but serializable by pycdr2 +@dataclass +class AddTwoInts_Request(IdlStruct, typename="AddTwoInts_Request"): + a: pycdr2.types.int64 + b: pycdr2.types.int64 + +# Equivalent to AddTwoInts.Response class, but serializable by pycdr2 +@dataclass +class AddTwoInts_Response(IdlStruct, typename="AddTwoInts_Request"): + sum: pycdr2.types.int64 + +def main(): + parser = argparse.ArgumentParser( + prog='add_two_ints_client', + description='Zenoh/ROS2 add_two_ints_client example') + parser.add_argument('--config', '-c', dest='config', + metavar='FILE', + type=str, + help='A configuration file.') + args = parser.parse_args() + + # Create Zenoh Config from file if provoded, or a default one otherwise + conf = zenoh.Config.from_file(args.config) if args.config is not None else zenoh.Config() + # Open Zenoh Session + session = zenoh.open(conf) + + req = AddTwoInts_Request(a=2, b=3) + # Send the query with the serialized request + replies = session.get('add_two_ints', 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: + # Deserialize the response + rep = AddTwoInts_Response.deserialize(reply.ok.payload) + print('Result of add_two_ints: %d' % rep.sum) + + session.close() + + +if __name__ == '__main__': + main() diff --git a/examples/python/fibonnacci_action_client.py b/examples/python/fibonnacci_action_client.py new file mode 100644 index 0000000..9a04791 --- /dev/null +++ b/examples/python/fibonnacci_action_client.py @@ -0,0 +1,112 @@ +# +# Copyright (c) 2024 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, +# +import time +import argparse +import zenoh + +# pycdr2 is the serializer of data in CDR format (required by ROS2) +import pycdr2 +from pycdr2 import IdlStruct +from dataclasses import dataclass + + +@dataclass +class Time(IdlStruct, typename="Time"): + sec: pycdr2.types.uint32 + nsec: pycdr2.types.uint32 + +# Equivalent to Fibonnaci.Goal.Request class, but serializable by pycdr2 +@dataclass +class Fibonacci_SendGoal_Request(IdlStruct, typename="Fibonacci_SendGoal_Request"): + goal_id: pycdr2.types.array[pycdr2.types.uint8, 16] + order: pycdr2.types.int32 + +# Equivalent to Fibonnaci.Goal.Response class, but serializable by pycdr2 +@dataclass +class Fibonacci_SendGoal_Response(IdlStruct, typename="Fibonacci_SendGoal_Response"): + accepted: bool + stamp: Time + +# Equivalent to Fibonnaci.Goal.Request class, but serializable by pycdr2 +@dataclass +class Fibonacci_GetResult_Request(IdlStruct, typename="Fibonacci_GetResult_Request"): + goal_id: pycdr2.types.array[pycdr2.types.uint8, 16] + +# Equivalent to Fibonnaci.Goal.Response class, but serializable by pycdr2 +@dataclass +class Fibonacci_GetResult_Response(IdlStruct, typename="Fibonacci_GetResult_Response"): + status: pycdr2.types.int8 + sequence: pycdr2.types.sequence[pycdr2.types.int32] + +@dataclass +class Fibonacci_Feedback(IdlStruct, typename="Fibonacci_Feedback"): + goal_id: pycdr2.types.array[pycdr2.types.uint8, 16] + partial_sequence: pycdr2.types.sequence[pycdr2.types.int32] + + + + +def feedback_callback(sample: zenoh.Sample): + # Deserialize the message + feedback = Fibonacci_Feedback.deserialize(sample.payload) + print('Received feedback: {0}'.format(feedback.partial_sequence)) + + +def main(): + parser = argparse.ArgumentParser( + prog='fibonacci_action_client', + description='Zenoh/ROS2 fibonacci_action_client example') + parser.add_argument('--config', '-c', dest='config', + metavar='FILE', + type=str, + help='A configuration file.') + args = parser.parse_args() + + # Create Zenoh Config from file if provoded, or a default one otherwise + conf = zenoh.Config.from_file(args.config) if args.config is not None else zenoh.Config() + # Open Zenoh Session + session = zenoh.open(conf) + + # Declare a subscriber for feedbacks + pub = session.declare_subscriber('fibonacci/_action/feedback', feedback_callback) + + 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 + 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: + # Deserialize the response + rep = Fibonacci_SendGoal_Response.deserialize(reply.ok.payload) + if not rep.accepted: + print('Goal rejected :(') + return + + print('Goal accepted :)') + + req = Fibonacci_GetResult_Request(goal_id) + # Send the query with the serialized request + replies = session.get('fibonacci/_action/get_result', 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: + # Deserialize the response + rep = Fibonacci_GetResult_Response.deserialize(reply.ok.payload) + print('Result: {0}'.format(rep.sequence)) + + + session.close() + + +if __name__ == '__main__': + main() diff --git a/examples/python/listener.py b/examples/python/listener.py new file mode 100644 index 0000000..7818f7c --- /dev/null +++ b/examples/python/listener.py @@ -0,0 +1,61 @@ +# +# Copyright (c) 2024 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, +# +import time +import argparse +import zenoh + +# pycdr2 is the serializer of data in CDR format (required by ROS2) +from pycdr2 import IdlStruct +from dataclasses import dataclass + +# Equivalent to std_msgs.msg.String class, but serializable by pycdr2 +@dataclass +class String(IdlStruct, typename="String"): + data: str + +def chatter_callback(sample: zenoh.Sample): + # Deserialize the message + msg = String.deserialize(sample.payload) + print('I heard: [%s]' % msg.data) + +def main(): + parser = argparse.ArgumentParser( + prog='listener', + description='Zenoh/ROS2 listener example') + parser.add_argument('--config', '-c', dest='config', + metavar='FILE', + type=str, + help='A configuration file.') + args = parser.parse_args() + + # Create Zenoh Config from file if provoded, or a default one otherwise + conf = zenoh.Config.from_file(args.config) if args.config is not None else zenoh.Config() + # Open Zenoh Session + session = zenoh.open(conf) + + # Declare a subscriber + pub = session.declare_subscriber('chatter', chatter_callback) + + try: + while True: + time.sleep(1) + except (KeyboardInterrupt): + pass + finally: + pub.undeclare() + session.close() + + +if __name__ == '__main__': + main() diff --git a/examples/python/talker.py b/examples/python/talker.py new file mode 100644 index 0000000..1c294d4 --- /dev/null +++ b/examples/python/talker.py @@ -0,0 +1,63 @@ +# +# Copyright (c) 2024 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, +# +import time +import argparse +import zenoh + +# pycdr2 is the serializer of data in CDR format (required by ROS2) +from pycdr2 import IdlStruct +from dataclasses import dataclass + +# Equivalent to std_msgs.msg.String class, but serializable by pycdr2 +@dataclass +class String(IdlStruct, typename="String"): + data: str + +def main(): + parser = argparse.ArgumentParser( + prog='talker', + description='Zenoh/ROS2 talker example') + parser.add_argument('--config', '-c', dest='config', + metavar='FILE', + type=str, + help='A configuration file.') + args = parser.parse_args() + + # Create Zenoh Config from file if provoded, or a default one otherwise + conf = zenoh.Config.from_file(args.config) if args.config is not None else zenoh.Config() + # Open Zenoh Session + session = zenoh.open(conf) + + # Declare a publisher (optional but allows Zenoh to perform some optimizations) + pub = session.declare_publisher('chatter') + + try: + i = 0 + while True: + i += 1 + msg = String(data='Hello World: {0}'.format(i)) + print('Publishing: "{0}"'.format(msg.data)) + # Publish the serialized message + pub.put(msg.serialize()) + time.sleep(1) + + except (KeyboardInterrupt): + pass + finally: + pub.undeclare() + session.close() + + +if __name__ == '__main__': + main()