From 7f2b51337fc6209e84a9475c627ca01b34fe69ae Mon Sep 17 00:00:00 2001 From: deepanshu Date: Thu, 30 Jun 2022 16:39:02 -0400 Subject: [PATCH 01/19] initial work on cli tool for service echo Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 142 +++++++++++++++++++++++++++ ros2service/setup.py | 1 + 2 files changed, 143 insertions(+) create mode 100644 ros2service/ros2service/verb/echo.py diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py new file mode 100644 index 000000000..d52d9193e --- /dev/null +++ b/ros2service/ros2service/verb/echo.py @@ -0,0 +1,142 @@ +# Copyright 2022 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import importlib +from typing import Optional +from typing import TypeVar + +import rclpy +from rclpy.node import Node +from rclpy.qos import QoSDurabilityPolicy +from rclpy.qos import QoSPresetProfiles +from rclpy.qos import QoSProfile +from rclpy.serialization import deserialize_message + +from rcl_interfaces.msg import ServiceEventType + +from ros2topic.api import unsigned_int + +from ros2service.api import ServiceNameCompleter +from ros2service.api import ServiceTypeCompleter +from ros2service.verb import VerbExtension +from rosidl_runtime_py.utilities import get_message +from ros2cli.node.strategy import NodeStrategy + +from rosidl_runtime_py import message_to_yaml +import yaml + +DEFAULT_TRUNCATE_LENGTH = 128 +MsgType = TypeVar('MsgType') + + +class EchoVerb(VerbExtension): + """Echo a service.""" + + def __init__(self): + super().__init__() + self.srv_module = None + self.topic_name = None + self.no_str = None + self.no_arr = None + self.truncate_length = None + self.flow_style = None + self.hidden_topic_suffix = "/_service_event" + self.message_type = get_message("rcl_interfaces/msg/ServiceEvent") + self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") + + def add_arguments(self, parser, cli_name): + arg = parser.add_argument( + 'service_name', + help="Name of the ROS service to echo from (e.g. '/add_two_ints')") + arg.completer = ServiceNameCompleter( + include_hidden_services_key='include_hidden_services') + arg = parser.add_argument( + 'service_type', nargs='?', + help="Type of the ROS service (e.g. 'std_srvs/srv/Empty')") + arg.completer = ServiceTypeCompleter( + service_name_key='service_name') + parser.add_argument( + '--full-length', '-f', action='store_true', + help='Output all elements for arrays, bytes, and string with a ' + "length > '--truncate-length', by default they are truncated " + "after '--truncate-length' elements with '...''") + parser.add_argument( + '--truncate-length', '-l', type=unsigned_int, default=DEFAULT_TRUNCATE_LENGTH, + help='The length to truncate arrays, bytes, and string to ' + '(default: %d)' % DEFAULT_TRUNCATE_LENGTH) + parser.add_argument( + '--no-arr', action='store_true', help="Don't print array fields of messages") + parser.add_argument( + '--no-str', action='store_true', help="Don't print string fields of messages") + parser.add_argument( + '--include-message-info', '-i', action='store_true', + help='Shows the associated message info.') + + def main(self, *, args): + self.topic_name = args.service_name + self.hidden_topic_suffix + self.truncate_length = args.truncate_length if not args.full_length else None + self.no_arr = args.no_arr + self.no_str = args.no_str + + try: + parts = args.service_type.split('/') + if len(parts) == 2: + parts = [parts[0], 'srv', parts[1]] + package_name = parts[0] + print(parts[:-1]) + module = importlib.import_module('.'.join(parts[:-1])) + srv_name = parts[-1] + self.srv_module = getattr(module, srv_name) + except (AttributeError, ModuleNotFoundError, ValueError): + raise RuntimeError('The passed service type is invalid') + try: + var = self.srv_module.Request + var = self.srv_module.Response + except AttributeError: + raise RuntimeError('The passed type is not a service') + + with NodeStrategy(args) as node: + self.subscribe_and_spin( + node, + self.topic_name, + self.message_type + ) + + def subscribe_and_spin( + self, + node: Node, + topic_name: str, + message_type: MsgType, + ) -> Optional[str]: + """Initialize a node with a single subscription and spin.""" + + node.create_subscription( + message_type, + topic_name, + self._subscriber_callback, + self.qos_profile) + + rclpy.spin(node) + + def _subscriber_callback(self, msg, info): + service_event_type = msg.info.event_type + serialized_event = b''.join(msg.serialized_event) + + if service_event_type is ServiceEventType.REQUEST_RECEIVED or \ + service_event_type is ServiceEventType.REQUEST_SENT: + service_request = deserialize_message(serialized_event, + self.srv_module.Request) + elif service_event_type is ServiceEventType.RESPONSE_RECEIVED or \ + service_event_type is ServiceEventType.RESPONSE_SENT: + service_request = deserialize_message(serialized_event, + self.srv_module.Response) diff --git a/ros2service/setup.py b/ros2service/setup.py index 2cdc3f5a3..430ab92ab 100644 --- a/ros2service/setup.py +++ b/ros2service/setup.py @@ -44,6 +44,7 @@ 'find = ros2service.verb.find:FindVerb', 'list = ros2service.verb.list:ListVerb', 'type = ros2service.verb.type:TypeVerb', + 'echo = ros2service.verb.echo:EchoVerb', ], } ) From 3b44aa055bbd9386f570e08e7cd48830a427c81e Mon Sep 17 00:00:00 2001 From: deepanshu Date: Fri, 1 Jul 2022 16:35:23 -0400 Subject: [PATCH 02/19] deserailize the data before printing Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 51 ++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index d52d9193e..2747c18ad 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -23,6 +23,7 @@ from rclpy.serialization import deserialize_message from rcl_interfaces.msg import ServiceEventType +from rcl_interfaces.msg import ServiceEvent from ros2topic.api import unsigned_int @@ -32,7 +33,7 @@ from rosidl_runtime_py.utilities import get_message from ros2cli.node.strategy import NodeStrategy -from rosidl_runtime_py import message_to_yaml +from rosidl_runtime_py import message_to_yaml, message_to_csv import yaml DEFAULT_TRUNCATE_LENGTH = 128 @@ -50,6 +51,8 @@ def __init__(self): self.no_arr = None self.truncate_length = None self.flow_style = None + self.csv = None + self.include_message_info = None self.hidden_topic_suffix = "/_service_event" self.message_type = get_message("rcl_interfaces/msg/ServiceEvent") self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") @@ -78,6 +81,16 @@ def add_arguments(self, parser, cli_name): '--no-arr', action='store_true', help="Don't print array fields of messages") parser.add_argument( '--no-str', action='store_true', help="Don't print string fields of messages") + parser.add_argument( + '--csv', action='store_true', + help=( + 'Output all recursive fields separated by commas (e.g. for ' + 'plotting). ' + 'If --include-message-info is also passed, the following fields are prepended: ' + 'source_timestamp, received_timestamp, publication_sequence_number,' + ' reception_sequence_number.' + ) + ) parser.add_argument( '--include-message-info', '-i', action='store_true', help='Shows the associated message info.') @@ -87,13 +100,14 @@ def main(self, *, args): self.truncate_length = args.truncate_length if not args.full_length else None self.no_arr = args.no_arr self.no_str = args.no_str + self.csv = args.csv + self.include_message_info = args.include_message_info try: parts = args.service_type.split('/') if len(parts) == 2: parts = [parts[0], 'srv', parts[1]] package_name = parts[0] - print(parts[:-1]) module = importlib.import_module('.'.join(parts[:-1])) srv_name = parts[-1] self.srv_module = getattr(module, srv_name) @@ -130,13 +144,36 @@ def subscribe_and_spin( def _subscriber_callback(self, msg, info): service_event_type = msg.info.event_type - serialized_event = b''.join(msg.serialized_event) + serialize_msg_type = None if service_event_type is ServiceEventType.REQUEST_RECEIVED or \ service_event_type is ServiceEventType.REQUEST_SENT: - service_request = deserialize_message(serialized_event, - self.srv_module.Request) + serialize_msg_type = self.srv_module.Request elif service_event_type is ServiceEventType.RESPONSE_RECEIVED or \ service_event_type is ServiceEventType.RESPONSE_SENT: - service_request = deserialize_message(serialized_event, - self.srv_module.Response) + serialize_msg_type = self.srv_module.Response + else: # TODO remove this else condition later + print("# Invalid Type") + return + + # csv: TODO + if self.csv: + to_print = message_to_csv( + msg, + truncate_length=self.truncate_length, + no_arr=self.no_arr, + no_str=self.no_str) + if self.include_message_info: + to_print = f'{",".join(str(x) for x in info.values())},{to_print}' + print(to_print) + return + + # yaml + if self.include_message_info: + print(yaml.dump(info), end='---\n') + print( + message_to_yaml( + msg, truncate_length=self.truncate_length, + no_arr=self.no_arr, no_str=self.no_str, flow_style=self.flow_style, + serialize_msg_type=serialize_msg_type), + end='---\n') From 39915f9fdf71f06751ca40f265aab4d90d112dff Mon Sep 17 00:00:00 2001 From: deepanshu Date: Sat, 2 Jul 2022 12:36:19 -0400 Subject: [PATCH 03/19] deserialization with csv option Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 2747c18ad..c367e751b 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -152,17 +152,18 @@ def _subscriber_callback(self, msg, info): elif service_event_type is ServiceEventType.RESPONSE_RECEIVED or \ service_event_type is ServiceEventType.RESPONSE_SENT: serialize_msg_type = self.srv_module.Response - else: # TODO remove this else condition later - print("# Invalid Type") + else: # TODO remove this else condition later once event enum is correct return - # csv: TODO + # csv if self.csv: to_print = message_to_csv( msg, truncate_length=self.truncate_length, no_arr=self.no_arr, - no_str=self.no_str) + no_str=self.no_str, + serialize_msg_type=serialize_msg_type + ) if self.include_message_info: to_print = f'{",".join(str(x) for x in info.values())},{to_print}' print(to_print) @@ -173,7 +174,10 @@ def _subscriber_callback(self, msg, info): print(yaml.dump(info), end='---\n') print( message_to_yaml( - msg, truncate_length=self.truncate_length, - no_arr=self.no_arr, no_str=self.no_str, flow_style=self.flow_style, + msg, + truncate_length=self.truncate_length, + no_arr=self.no_arr, + no_str=self.no_str, + flow_style=self.flow_style, serialize_msg_type=serialize_msg_type), end='---\n') From 3eb85f3501b42025b22e66b7c399dab62ce07460 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Sat, 2 Jul 2022 13:52:38 -0400 Subject: [PATCH 04/19] try getting service type from service name Signed-off-by: deepanshu --- ros2service/ros2service/api/__init__.py | 3 ++ ros2service/ros2service/verb/echo.py | 37 +++++++++++++++++++++---- ros2topic/ros2topic/verb/echo.py | 6 ++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/ros2service/ros2service/api/__init__.py b/ros2service/ros2service/api/__init__.py index 9143ca163..9a1f2ff4e 100644 --- a/ros2service/ros2service/api/__init__.py +++ b/ros2service/ros2service/api/__init__.py @@ -33,6 +33,9 @@ def get_service_names(*, node, include_hidden_services=False): node=node, include_hidden_services=include_hidden_services) return [n for (n, t) in service_names_and_types] +def get_service_type(*, node, service, include_hidden_services=False): + pass + def service_type_completer(**kwargs): """Callable returning a list of service types.""" diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index c367e751b..45a75d6bb 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -27,7 +27,7 @@ from ros2topic.api import unsigned_int -from ros2service.api import ServiceNameCompleter +from ros2service.api import ServiceNameCompleter, get_service_names_and_types from ros2service.api import ServiceTypeCompleter from ros2service.verb import VerbExtension from rosidl_runtime_py.utilities import get_message @@ -45,13 +45,14 @@ class EchoVerb(VerbExtension): def __init__(self): super().__init__() - self.srv_module = None - self.topic_name = None self.no_str = None self.no_arr = None self.truncate_length = None self.flow_style = None self.csv = None + self.srv_module = None + self.srv_type = None + self.topic_name = None self.include_message_info = None self.hidden_topic_suffix = "/_service_event" self.message_type = get_message("rcl_interfaces/msg/ServiceEvent") @@ -103,8 +104,33 @@ def main(self, *, args): self.csv = args.csv self.include_message_info = args.include_message_info + if args.service_type is None: + with NodeStrategy(args) as node: + service_names_and_types = get_service_names_and_types( + node=node, + include_hidden_services=True) + + for (service_name, service_types) in service_names_and_types: + if args.service_name == service_name: + if len(service_types) is not 1: + raise RuntimeError( + 'Found multiple types for the same service, try specifying ' + 'the type using service_type positional argument') + for service_type in service_types: + self.srv_type = service_type + if self.srv_type is None: + raise RuntimeError( + 'Could not find running instance the service') + else: + self.srv_type = args.service_type + + if self.srv_type is None: + raise RuntimeError( + 'Could not determine the type for the passed service') + + # load the service reqeust and response module try: - parts = args.service_type.split('/') + parts = self.srv_type.split('/') if len(parts) == 2: parts = [parts[0], 'srv', parts[1]] package_name = parts[0] @@ -152,7 +178,8 @@ def _subscriber_callback(self, msg, info): elif service_event_type is ServiceEventType.RESPONSE_RECEIVED or \ service_event_type is ServiceEventType.RESPONSE_SENT: serialize_msg_type = self.srv_module.Response - else: # TODO remove this else condition later once event enum is correct + else: # TODO remove this once event enum is correct + print("Returning invalid service event type") return # csv diff --git a/ros2topic/ros2topic/verb/echo.py b/ros2topic/ros2topic/verb/echo.py index 0eb7c1c0a..21251d27a 100644 --- a/ros2topic/ros2topic/verb/echo.py +++ b/ros2topic/ros2topic/verb/echo.py @@ -230,9 +230,15 @@ def main(self, *, args): if args.message_type is None: message_type = get_msg_class( node, args.topic_name, include_hidden_topics=True) + print("Debug 1") + print(message_type) + print(type(message_type)) else: + print("Debug 2") try: message_type = get_message(args.message_type) + print(message_type) + print(type(message_type)) except (AttributeError, ModuleNotFoundError, ValueError): raise RuntimeError('The passed message type is invalid') From 6fbb8a7b3f28d8e7cb1041e529dca9c54c373ca3 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Sat, 2 Jul 2022 14:42:52 -0400 Subject: [PATCH 05/19] Aded API for loading service module from srv type Signed-off-by: deepanshu --- ros2service/ros2service/api/__init__.py | 46 ++++++++++++++++- ros2service/ros2service/verb/echo.py | 67 +++++++------------------ 2 files changed, 63 insertions(+), 50 deletions(-) diff --git a/ros2service/ros2service/api/__init__.py b/ros2service/ros2service/api/__init__.py index 9a1f2ff4e..ae4d01391 100644 --- a/ros2service/ros2service/api/__init__.py +++ b/ros2service/ros2service/api/__init__.py @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from time import sleep from rclpy.topic_or_service_is_hidden import topic_or_service_is_hidden from ros2cli.node.strategy import NodeStrategy from rosidl_runtime_py import get_service_interfaces from rosidl_runtime_py import message_to_yaml from rosidl_runtime_py.utilities import get_service +import rclpy + def get_service_names_and_types(*, node, include_hidden_services=False): service_names_and_types = node.get_service_names_and_types() @@ -33,8 +36,47 @@ def get_service_names(*, node, include_hidden_services=False): node=node, include_hidden_services=include_hidden_services) return [n for (n, t) in service_names_and_types] -def get_service_type(*, node, service, include_hidden_services=False): - pass + +def get_service_class(node, service, blocking=False, include_hidden_services=False): + srv_class = _get_service_class(node, service, include_hidden_services) + if srv_class: + return srv_class + elif blocking: + print('WARNING: service [%s] does not appear to be started yet' % service) + while rclpy.ok(): + srv_class = _get_service_class(node, service, include_hidden_services) + if srv_class: + return srv_class + else: + sleep(0.1) + else: + print('WARNING: service [%s] does not appear to be started yet' % service) + return None + + +def _get_service_class(node, service, include_hidden_services): + service_names_and_types = get_service_names_and_types( + node=node, + include_hidden_services=include_hidden_services) + + service_type = None + for (service_name, service_types) in service_names_and_types: + if service == service_name: + if len(service_types) > 1: + raise RuntimeError( + "Cannot echo service '%s', as it contains more than one type: [%s]" % + (service, ', '.join(service_types)) + ) + service_type = service_types[0] + break + + if service_type is None: + return None + + try: + return get_service(service_type) + except (AttributeError, ModuleNotFoundError, ValueError): + raise RuntimeError("The service type '%s' is invalid" % service_type) def service_type_completer(**kwargs): diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 45a75d6bb..c03b7b53f 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -27,10 +27,10 @@ from ros2topic.api import unsigned_int -from ros2service.api import ServiceNameCompleter, get_service_names_and_types +from ros2service.api import ServiceNameCompleter, get_service_names_and_types, get_service_class from ros2service.api import ServiceTypeCompleter from ros2service.verb import VerbExtension -from rosidl_runtime_py.utilities import get_message +from rosidl_runtime_py.utilities import get_message, get_service from ros2cli.node.strategy import NodeStrategy from rosidl_runtime_py import message_to_yaml, message_to_csv @@ -51,11 +51,8 @@ def __init__(self): self.flow_style = None self.csv = None self.srv_module = None - self.srv_type = None - self.topic_name = None self.include_message_info = None - self.hidden_topic_suffix = "/_service_event" - self.message_type = get_message("rcl_interfaces/msg/ServiceEvent") + self.event_msg_type = get_message("rcl_interfaces/msg/ServiceEvent") self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") def add_arguments(self, parser, cli_name): @@ -97,7 +94,6 @@ def add_arguments(self, parser, cli_name): help='Shows the associated message info.') def main(self, *, args): - self.topic_name = args.service_name + self.hidden_topic_suffix self.truncate_length = args.truncate_length if not args.full_length else None self.no_arr = args.no_arr self.no_str = args.no_str @@ -106,63 +102,38 @@ def main(self, *, args): if args.service_type is None: with NodeStrategy(args) as node: - service_names_and_types = get_service_names_and_types( - node=node, - include_hidden_services=True) - - for (service_name, service_types) in service_names_and_types: - if args.service_name == service_name: - if len(service_types) is not 1: - raise RuntimeError( - 'Found multiple types for the same service, try specifying ' - 'the type using service_type positional argument') - for service_type in service_types: - self.srv_type = service_type - if self.srv_type is None: - raise RuntimeError( - 'Could not find running instance the service') + self.srv_module = get_service_class( + node, args.service_name, blocking=False, include_hidden_services=True) else: - self.srv_type = args.service_type + try: + self.srv_module = get_service(args.service_type) + except (AttributeError, ModuleNotFoundError, ValueError): + raise RuntimeError("The service type '%s' is invalid" % args.service_type) - if self.srv_type is None: + if self.srv_module is None: raise RuntimeError( - 'Could not determine the type for the passed service') - - # load the service reqeust and response module - try: - parts = self.srv_type.split('/') - if len(parts) == 2: - parts = [parts[0], 'srv', parts[1]] - package_name = parts[0] - module = importlib.import_module('.'.join(parts[:-1])) - srv_name = parts[-1] - self.srv_module = getattr(module, srv_name) - except (AttributeError, ModuleNotFoundError, ValueError): - raise RuntimeError('The passed service type is invalid') - try: - var = self.srv_module.Request - var = self.srv_module.Response - except AttributeError: - raise RuntimeError('The passed type is not a service') + 'Could not load the type for the passed service') + + event_topic_name = args.service_name + "/_service_event" with NodeStrategy(args) as node: self.subscribe_and_spin( node, - self.topic_name, - self.message_type + event_topic_name, + self.event_msg_type ) def subscribe_and_spin( self, node: Node, - topic_name: str, - message_type: MsgType, + event_topic_name: str, + event_msg_type: MsgType, ) -> Optional[str]: """Initialize a node with a single subscription and spin.""" node.create_subscription( - message_type, - topic_name, + event_msg_type, + event_topic_name, self._subscriber_callback, self.qos_profile) From f1c202fc18d97356b89a537d1df5ade27221f262 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Sat, 2 Jul 2022 14:44:22 -0400 Subject: [PATCH 06/19] revert ros2topic debug changes Signed-off-by: deepanshu --- ros2topic/ros2topic/verb/echo.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ros2topic/ros2topic/verb/echo.py b/ros2topic/ros2topic/verb/echo.py index 21251d27a..0eb7c1c0a 100644 --- a/ros2topic/ros2topic/verb/echo.py +++ b/ros2topic/ros2topic/verb/echo.py @@ -230,15 +230,9 @@ def main(self, *, args): if args.message_type is None: message_type = get_msg_class( node, args.topic_name, include_hidden_topics=True) - print("Debug 1") - print(message_type) - print(type(message_type)) else: - print("Debug 2") try: message_type = get_message(args.message_type) - print(message_type) - print(type(message_type)) except (AttributeError, ModuleNotFoundError, ValueError): raise RuntimeError('The passed message type is invalid') From c382d67001cbde94be97cb9f4d827616a9744698 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 5 Jul 2022 10:00:08 -0400 Subject: [PATCH 07/19] Added client and server only optional arguments Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 34 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index c03b7b53f..f98e0db4f 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -47,11 +47,13 @@ def __init__(self): super().__init__() self.no_str = None self.no_arr = None - self.truncate_length = None - self.flow_style = None self.csv = None - self.srv_module = None + self.flow_style = None + self.client = None + self.server = None + self.truncate_length = None self.include_message_info = None + self.srv_module = None self.event_msg_type = get_message("rcl_interfaces/msg/ServiceEvent") self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") @@ -92,6 +94,10 @@ def add_arguments(self, parser, cli_name): parser.add_argument( '--include-message-info', '-i', action='store_true', help='Shows the associated message info.') + parser.add_argument( + '--client', action='store_true', help="Echo only request sent or response received by service client") + parser.add_argument( + '--server', action='store_true', help="Echo only request received or response sent by service server") def main(self, *, args): self.truncate_length = args.truncate_length if not args.full_length else None @@ -99,6 +105,8 @@ def main(self, *, args): self.no_str = args.no_str self.csv = args.csv self.include_message_info = args.include_message_info + self.client = args.client + self.server = args.server if args.service_type is None: with NodeStrategy(args) as node: @@ -140,14 +148,24 @@ def subscribe_and_spin( rclpy.spin(node) def _subscriber_callback(self, msg, info): - service_event_type = msg.info.event_type serialize_msg_type = None + event_enum = msg.info.event_type + + if self.client: + if event_enum is ServiceEventType.REQUEST_RECEIVED or \ + event_enum is ServiceEventType.RESPONSE_SENT: + return + + if self.server: + if event_enum is ServiceEventType.REQUEST_SENT or \ + event_enum is ServiceEventType.RESPONSE_RECEIVED: + return - if service_event_type is ServiceEventType.REQUEST_RECEIVED or \ - service_event_type is ServiceEventType.REQUEST_SENT: + if event_enum is ServiceEventType.REQUEST_RECEIVED or \ + event_enum is ServiceEventType.REQUEST_SENT: serialize_msg_type = self.srv_module.Request - elif service_event_type is ServiceEventType.RESPONSE_RECEIVED or \ - service_event_type is ServiceEventType.RESPONSE_SENT: + elif event_enum is ServiceEventType.RESPONSE_RECEIVED or \ + event_enum is ServiceEventType.RESPONSE_SENT: serialize_msg_type = self.srv_module.Response else: # TODO remove this once event enum is correct print("Returning invalid service event type") From 8a01dfb6e02013ce4a65284d2cadc073354668df Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 5 Jul 2022 10:04:37 -0400 Subject: [PATCH 08/19] Minor formatting and logic change for prev commit Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index f98e0db4f..10123b369 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import importlib + from typing import Optional from typing import TypeVar @@ -151,12 +152,13 @@ def _subscriber_callback(self, msg, info): serialize_msg_type = None event_enum = msg.info.event_type - if self.client: + if self.client and self.server: + pass + elif self.client: if event_enum is ServiceEventType.REQUEST_RECEIVED or \ event_enum is ServiceEventType.RESPONSE_SENT: return - - if self.server: + elif self.server: if event_enum is ServiceEventType.REQUEST_SENT or \ event_enum is ServiceEventType.RESPONSE_RECEIVED: return From e32776875273de8652002bcba56f401c4b0b1a52 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 5 Jul 2022 10:08:56 -0400 Subject: [PATCH 09/19] Remove unused import statements Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 10123b369..f555b2f43 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -11,30 +11,25 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import importlib from typing import Optional from typing import TypeVar import rclpy from rclpy.node import Node -from rclpy.qos import QoSDurabilityPolicy from rclpy.qos import QoSPresetProfiles -from rclpy.qos import QoSProfile -from rclpy.serialization import deserialize_message - -from rcl_interfaces.msg import ServiceEventType -from rcl_interfaces.msg import ServiceEvent +from ros2cli.node.strategy import NodeStrategy from ros2topic.api import unsigned_int +from rcl_interfaces.msg import ServiceEventType -from ros2service.api import ServiceNameCompleter, get_service_names_and_types, get_service_class +from ros2service.api import ServiceNameCompleter, get_service_class from ros2service.api import ServiceTypeCompleter from ros2service.verb import VerbExtension -from rosidl_runtime_py.utilities import get_message, get_service -from ros2cli.node.strategy import NodeStrategy from rosidl_runtime_py import message_to_yaml, message_to_csv +from rosidl_runtime_py.utilities import get_message, get_service + import yaml DEFAULT_TRUNCATE_LENGTH = 128 From 3e3e72a2eaad76ddc7f77ae3c4fd7cd167e8f6ab Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 5 Jul 2022 10:48:37 -0400 Subject: [PATCH 10/19] format output testing Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 57 ++++++++++++++++++---------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index f555b2f43..7d240f1a4 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -11,10 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import sys from typing import Optional from typing import TypeVar +from collections import OrderedDict + import rclpy from rclpy.node import Node from rclpy.qos import QoSPresetProfiles @@ -27,7 +29,7 @@ from ros2service.api import ServiceTypeCompleter from ros2service.verb import VerbExtension -from rosidl_runtime_py import message_to_yaml, message_to_csv +from rosidl_runtime_py import message_to_yaml, message_to_csv, message_to_ordereddict from rosidl_runtime_py.utilities import get_message, get_service import yaml @@ -50,6 +52,7 @@ def __init__(self): self.truncate_length = None self.include_message_info = None self.srv_module = None + self.event_enum = None self.event_msg_type = get_message("rcl_interfaces/msg/ServiceEvent") self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") @@ -145,24 +148,24 @@ def subscribe_and_spin( def _subscriber_callback(self, msg, info): serialize_msg_type = None - event_enum = msg.info.event_type + self.event_enum = msg.info.event_type if self.client and self.server: pass elif self.client: - if event_enum is ServiceEventType.REQUEST_RECEIVED or \ - event_enum is ServiceEventType.RESPONSE_SENT: + if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ + self.event_enum is ServiceEventType.RESPONSE_SENT: return elif self.server: - if event_enum is ServiceEventType.REQUEST_SENT or \ - event_enum is ServiceEventType.RESPONSE_RECEIVED: + if self.event_enum is ServiceEventType.REQUEST_SENT or \ + self.event_enum is ServiceEventType.RESPONSE_RECEIVED: return - if event_enum is ServiceEventType.REQUEST_RECEIVED or \ - event_enum is ServiceEventType.REQUEST_SENT: + if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ + self.event_enum is ServiceEventType.REQUEST_SENT: serialize_msg_type = self.srv_module.Request - elif event_enum is ServiceEventType.RESPONSE_RECEIVED or \ - event_enum is ServiceEventType.RESPONSE_SENT: + elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED or \ + self.event_enum is ServiceEventType.RESPONSE_SENT: serialize_msg_type = self.srv_module.Response else: # TODO remove this once event enum is correct print("Returning invalid service event type") @@ -186,11 +189,27 @@ def _subscriber_callback(self, msg, info): if self.include_message_info: print(yaml.dump(info), end='---\n') print( - message_to_yaml( - msg, - truncate_length=self.truncate_length, - no_arr=self.no_arr, - no_str=self.no_str, - flow_style=self.flow_style, - serialize_msg_type=serialize_msg_type), - end='---\n') + self.format_output(message_to_ordereddict( + msg, truncate_length=self.truncate_length, + no_arr=self.no_arr, no_str=self.no_str, serialize_msg_type=serialize_msg_type)), + end='---------------------------\n') + + def format_output(self, dict_service_event: OrderedDict): + dict_output = OrderedDict() + dict_output['event_type'] = dict_service_event['info']['event_name'] + + # dict_output = {'event_type': dict_service_event['info']['event_name'], + # 'stamp': dict_service_event['info']['stamp'], + # 'client_id': dict_service_event['info']['client_id'], + # 'sequence_number': dict_service_event['info']['sequence_number']} + + if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ + self.event_enum is ServiceEventType.REQUEST_SENT: + dict_output['request'] = dict_service_event['serialized_event'] + elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED or \ + self.event_enum is ServiceEventType.RESPONSE_SENT: + dict_output['response'] = dict_service_event['serialized_event'] + + return yaml.dump(dict_output, + allow_unicode=True, width=sys.maxsize, default_flow_style=self.flow_style, + ) From 5c30256f5f2e549c08785597812dbc4eb2485a0f Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 5 Jul 2022 14:50:51 -0400 Subject: [PATCH 11/19] format output msg for echo Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 37 ++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 7d240f1a4..9c1a1d154 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -29,7 +29,7 @@ from ros2service.api import ServiceTypeCompleter from ros2service.verb import VerbExtension -from rosidl_runtime_py import message_to_yaml, message_to_csv, message_to_ordereddict +from rosidl_runtime_py import message_to_csv, message_to_ordereddict from rosidl_runtime_py.utilities import get_message, get_service import yaml @@ -38,6 +38,13 @@ MsgType = TypeVar('MsgType') +def represent_ordereddict(dumper, data): + items = [] + for k, v in data.items(): + items.append((dumper.represent_data(k), dumper.represent_data(v))) + return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', items) + + class EchoVerb(VerbExtension): """Echo a service.""" @@ -55,6 +62,7 @@ def __init__(self): self.event_enum = None self.event_msg_type = get_message("rcl_interfaces/msg/ServiceEvent") self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") + self.__yaml_representer_registered = False def add_arguments(self, parser, cli_name): arg = parser.add_argument( @@ -196,12 +204,18 @@ def _subscriber_callback(self, msg, info): def format_output(self, dict_service_event: OrderedDict): dict_output = OrderedDict() - dict_output['event_type'] = dict_service_event['info']['event_name'] - - # dict_output = {'event_type': dict_service_event['info']['event_name'], - # 'stamp': dict_service_event['info']['stamp'], - # 'client_id': dict_service_event['info']['client_id'], - # 'sequence_number': dict_service_event['info']['sequence_number']} + dict_output['stamp'] = dict_service_event['info']['stamp'] + dict_output['client_id'] = dict_service_event['info']['client_id'] + dict_output['sequence_number'] = dict_service_event['info']['sequence_number'] + + if self.event_enum is ServiceEventType.REQUEST_SENT: + dict_output['event_type'] = 'CLIENT_REQUEST_SENT' + elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED: + dict_output['event_type'] = 'CLIENT_RESPONSE_RECEIVED' + elif self.event_enum is ServiceEventType.REQUEST_RECEIVED: + dict_output['event_type'] = 'SERVER_REQUEST_RECEIVED' + elif self.event_enum is ServiceEventType.RESPONSE_SENT: + dict_output['event_type'] = 'SERVER_RESPONSE_SENT' if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ self.event_enum is ServiceEventType.REQUEST_SENT: @@ -210,6 +224,13 @@ def format_output(self, dict_service_event: OrderedDict): self.event_enum is ServiceEventType.RESPONSE_SENT: dict_output['response'] = dict_service_event['serialized_event'] + # Register custom representer for YAML output + if not self.__yaml_representer_registered: + yaml.add_representer(OrderedDict, represent_ordereddict) + self.__yaml_representer_registered = True + return yaml.dump(dict_output, - allow_unicode=True, width=sys.maxsize, default_flow_style=self.flow_style, + allow_unicode=True, + width=sys.maxsize, + default_flow_style=self.flow_style, ) From 02c9e93e264bbadaffe8a61b6ce8167e9d751f34 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 5 Jul 2022 14:53:59 -0400 Subject: [PATCH 12/19] Rename serialize_msg_type to derialize_msg_type Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 9c1a1d154..022183450 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -155,7 +155,7 @@ def subscribe_and_spin( rclpy.spin(node) def _subscriber_callback(self, msg, info): - serialize_msg_type = None + deserialize_msg_type = None self.event_enum = msg.info.event_type if self.client and self.server: @@ -171,10 +171,10 @@ def _subscriber_callback(self, msg, info): if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ self.event_enum is ServiceEventType.REQUEST_SENT: - serialize_msg_type = self.srv_module.Request + deserialize_msg_type = self.srv_module.Request elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED or \ self.event_enum is ServiceEventType.RESPONSE_SENT: - serialize_msg_type = self.srv_module.Response + deserialize_msg_type = self.srv_module.Response else: # TODO remove this once event enum is correct print("Returning invalid service event type") return @@ -186,7 +186,7 @@ def _subscriber_callback(self, msg, info): truncate_length=self.truncate_length, no_arr=self.no_arr, no_str=self.no_str, - serialize_msg_type=serialize_msg_type + deserialize_msg_type=deserialize_msg_type ) if self.include_message_info: to_print = f'{",".join(str(x) for x in info.values())},{to_print}' @@ -199,7 +199,7 @@ def _subscriber_callback(self, msg, info): print( self.format_output(message_to_ordereddict( msg, truncate_length=self.truncate_length, - no_arr=self.no_arr, no_str=self.no_str, serialize_msg_type=serialize_msg_type)), + no_arr=self.no_arr, no_str=self.no_str, deserialize_msg_type=deserialize_msg_type)), end='---------------------------\n') def format_output(self, dict_service_event: OrderedDict): From 29321db588f09965565eede176165db318451446 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Mon, 18 Jul 2022 10:24:31 -0400 Subject: [PATCH 13/19] changes after review comments Signed-off-by: deepanshu --- ros2service/ros2service/api/__init__.py | 29 ++++++---- ros2service/ros2service/verb/echo.py | 72 ++++++++++++------------- ros2service/setup.py | 2 +- 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/ros2service/ros2service/api/__init__.py b/ros2service/ros2service/api/__init__.py index ae4d01391..1584171e5 100644 --- a/ros2service/ros2service/api/__init__.py +++ b/ros2service/ros2service/api/__init__.py @@ -38,19 +38,29 @@ def get_service_names(*, node, include_hidden_services=False): def get_service_class(node, service, blocking=False, include_hidden_services=False): + """ + Load service type module for the given service. + + The service should be running for this function to find the service type. + :param node: The node object of rclpy Node class. + :param service: The name of the service. + :param blocking: If blocking is True this function will wait for the service to start. + :param include_hidden_services: Whether to include hidden services while finding the + list of currently running services. + :return: + """ srv_class = _get_service_class(node, service, include_hidden_services) - if srv_class: + if srv_class is not None: return srv_class elif blocking: - print('WARNING: service [%s] does not appear to be started yet' % service) + print(f'WARNING: service [{service}] does not appear to be started yet') while rclpy.ok(): srv_class = _get_service_class(node, service, include_hidden_services) - if srv_class: + if srv_class is not None: return srv_class - else: - sleep(0.1) + sleep(0.1) else: - print('WARNING: service [%s] does not appear to be started yet' % service) + print(f'WARNING: service [{service}] does not appear to be started yet') return None @@ -64,9 +74,8 @@ def _get_service_class(node, service, include_hidden_services): if service == service_name: if len(service_types) > 1: raise RuntimeError( - "Cannot echo service '%s', as it contains more than one type: [%s]" % - (service, ', '.join(service_types)) - ) + f"Cannot echo service '{service}', as it contains more than one " + f"type: [{', '.join(service_types)}]") service_type = service_types[0] break @@ -76,7 +85,7 @@ def _get_service_class(node, service, include_hidden_services): try: return get_service(service_type) except (AttributeError, ModuleNotFoundError, ValueError): - raise RuntimeError("The service type '%s' is invalid" % service_type) + raise RuntimeError(f"The service type '{service_type}' is invalid") def service_type_completer(**kwargs): diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 022183450..a106c7741 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import sys -from typing import Optional from typing import TypeVar from collections import OrderedDict @@ -59,31 +58,29 @@ def __init__(self): self.truncate_length = None self.include_message_info = None self.srv_module = None - self.event_enum = None - self.event_msg_type = get_message("rcl_interfaces/msg/ServiceEvent") - self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") + self.event_msg_type = get_message('rcl_interfaces/msg/ServiceEvent') + self.qos_profile = QoSPresetProfiles.get_from_short_key('services_default') self.__yaml_representer_registered = False def add_arguments(self, parser, cli_name): arg = parser.add_argument( 'service_name', - help="Name of the ROS service to echo from (e.g. '/add_two_ints')") + help="Name of the ROS service to echo (e.g. '/add_two_ints')") arg.completer = ServiceNameCompleter( include_hidden_services_key='include_hidden_services') arg = parser.add_argument( 'service_type', nargs='?', - help="Type of the ROS service (e.g. 'std_srvs/srv/Empty')") - arg.completer = ServiceTypeCompleter( - service_name_key='service_name') + help="Type of the ROS service (e.g. 'example_interfaces/srv/AddTwoInts')") + arg.completer = ServiceTypeCompleter(service_name_key='service_name') parser.add_argument( '--full-length', '-f', action='store_true', help='Output all elements for arrays, bytes, and string with a ' "length > '--truncate-length', by default they are truncated " - "after '--truncate-length' elements with '...''") + "after '--truncate-length' elements with '...'") parser.add_argument( '--truncate-length', '-l', type=unsigned_int, default=DEFAULT_TRUNCATE_LENGTH, help='The length to truncate arrays, bytes, and string to ' - '(default: %d)' % DEFAULT_TRUNCATE_LENGTH) + f'(default: {DEFAULT_TRUNCATE_LENGTH})') parser.add_argument( '--no-arr', action='store_true', help="Don't print array fields of messages") parser.add_argument( @@ -102,9 +99,11 @@ def add_arguments(self, parser, cli_name): '--include-message-info', '-i', action='store_true', help='Shows the associated message info.') parser.add_argument( - '--client', action='store_true', help="Echo only request sent or response received by service client") + '--client-only', action='store_true', + help="Echo only request sent or response received messages from service clients.") parser.add_argument( - '--server', action='store_true', help="Echo only request received or response sent by service server") + '--server-only', action='store_true', + help="Echo only request received or response sent messages from service servers.") def main(self, *, args): self.truncate_length = args.truncate_length if not args.full_length else None @@ -123,7 +122,7 @@ def main(self, *, args): try: self.srv_module = get_service(args.service_type) except (AttributeError, ModuleNotFoundError, ValueError): - raise RuntimeError("The service type '%s' is invalid" % args.service_type) + raise RuntimeError(f"The service type '{args.service_type}' is invalid") if self.srv_module is None: raise RuntimeError( @@ -143,9 +142,8 @@ def subscribe_and_spin( node: Node, event_topic_name: str, event_msg_type: MsgType, - ) -> Optional[str]: + ) -> None: """Initialize a node with a single subscription and spin.""" - node.create_subscription( event_msg_type, event_topic_name, @@ -156,28 +154,27 @@ def subscribe_and_spin( def _subscriber_callback(self, msg, info): deserialize_msg_type = None - self.event_enum = msg.info.event_type + event_enum = msg.info.event_type if self.client and self.server: pass elif self.client: - if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ - self.event_enum is ServiceEventType.RESPONSE_SENT: + if event_enum is ServiceEventType.REQUEST_RECEIVED or \ + event_enum is ServiceEventType.RESPONSE_SENT: return elif self.server: - if self.event_enum is ServiceEventType.REQUEST_SENT or \ - self.event_enum is ServiceEventType.RESPONSE_RECEIVED: + if event_enum is ServiceEventType.REQUEST_SENT or \ + event_enum is ServiceEventType.RESPONSE_RECEIVED: return - if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ - self.event_enum is ServiceEventType.REQUEST_SENT: + if event_enum is ServiceEventType.REQUEST_RECEIVED or \ + event_enum is ServiceEventType.REQUEST_SENT: deserialize_msg_type = self.srv_module.Request - elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED or \ - self.event_enum is ServiceEventType.RESPONSE_SENT: + elif event_enum is ServiceEventType.RESPONSE_RECEIVED or \ + event_enum is ServiceEventType.RESPONSE_SENT: deserialize_msg_type = self.srv_module.Response - else: # TODO remove this once event enum is correct - print("Returning invalid service event type") - return + else: + raise ValueError(f'received unexpected service event type {event_enum}') # csv if self.csv: @@ -199,29 +196,30 @@ def _subscriber_callback(self, msg, info): print( self.format_output(message_to_ordereddict( msg, truncate_length=self.truncate_length, - no_arr=self.no_arr, no_str=self.no_str, deserialize_msg_type=deserialize_msg_type)), + no_arr=self.no_arr, no_str=self.no_str, deserialize_msg_type=deserialize_msg_type), + event_enum), end='---------------------------\n') - def format_output(self, dict_service_event: OrderedDict): + def format_output(self, dict_service_event: OrderedDict, event_enum): dict_output = OrderedDict() dict_output['stamp'] = dict_service_event['info']['stamp'] dict_output['client_id'] = dict_service_event['info']['client_id'] dict_output['sequence_number'] = dict_service_event['info']['sequence_number'] - if self.event_enum is ServiceEventType.REQUEST_SENT: + if event_enum is ServiceEventType.REQUEST_SENT: dict_output['event_type'] = 'CLIENT_REQUEST_SENT' - elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED: + elif event_enum is ServiceEventType.RESPONSE_RECEIVED: dict_output['event_type'] = 'CLIENT_RESPONSE_RECEIVED' - elif self.event_enum is ServiceEventType.REQUEST_RECEIVED: + elif event_enum is ServiceEventType.REQUEST_RECEIVED: dict_output['event_type'] = 'SERVER_REQUEST_RECEIVED' - elif self.event_enum is ServiceEventType.RESPONSE_SENT: + elif event_enum is ServiceEventType.RESPONSE_SENT: dict_output['event_type'] = 'SERVER_RESPONSE_SENT' - if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ - self.event_enum is ServiceEventType.REQUEST_SENT: + if event_enum is ServiceEventType.REQUEST_RECEIVED or \ + event_enum is ServiceEventType.REQUEST_SENT: dict_output['request'] = dict_service_event['serialized_event'] - elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED or \ - self.event_enum is ServiceEventType.RESPONSE_SENT: + elif event_enum is ServiceEventType.RESPONSE_RECEIVED or \ + event_enum is ServiceEventType.RESPONSE_SENT: dict_output['response'] = dict_service_event['serialized_event'] # Register custom representer for YAML output diff --git a/ros2service/setup.py b/ros2service/setup.py index 430ab92ab..30dc011be 100644 --- a/ros2service/setup.py +++ b/ros2service/setup.py @@ -41,10 +41,10 @@ ], 'ros2service.verb': [ 'call = ros2service.verb.call:CallVerb', + 'echo = ros2service.verb.echo:EchoVerb', 'find = ros2service.verb.find:FindVerb', 'list = ros2service.verb.list:ListVerb', 'type = ros2service.verb.type:TypeVerb', - 'echo = ros2service.verb.echo:EchoVerb', ], } ) From 43c554f46d47d066b57f934c380385c83b5981f2 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Mon, 18 Jul 2022 11:05:35 -0400 Subject: [PATCH 14/19] clent only and server only arg correction Signed-off-by: deepanshu --- ros2service/ros2service/verb/echo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index a106c7741..1a2afc457 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -111,8 +111,8 @@ def main(self, *, args): self.no_str = args.no_str self.csv = args.csv self.include_message_info = args.include_message_info - self.client = args.client - self.server = args.server + self.client = args.client_only + self.server = args.server_only if args.service_type is None: with NodeStrategy(args) as node: From 0de98921d842cc39b059f275dca2dda1ea579bf1 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 19 Jul 2022 12:26:39 -0400 Subject: [PATCH 15/19] work in progress: unit test Signed-off-by: deepanshu --- ros2interface/test/test_cli.py | 3 ++- ros2service/test/fixtures/echo_client.py | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 ros2service/test/fixtures/echo_client.py diff --git a/ros2interface/test/test_cli.py b/ros2interface/test/test_cli.py index 8f1c91c5b..6bd2bf1ca 100644 --- a/ros2interface/test/test_cli.py +++ b/ros2interface/test/test_cli.py @@ -132,7 +132,8 @@ def test_list_messages(self): strict=False ) - def test_list_services(self): + def \ + test_list_services(self): with self.launch_interface_command(arguments=['list', '-s']) as interface_command: assert interface_command.wait_for_shutdown(timeout=2) assert interface_command.exit_code == launch_testing.asserts.EXIT_OK diff --git a/ros2service/test/fixtures/echo_client.py b/ros2service/test/fixtures/echo_client.py new file mode 100644 index 000000000..e69de29bb From 969c33fcfd63934c6dd18d6003d925452b723854 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 19 Jul 2022 12:27:05 -0400 Subject: [PATCH 16/19] work in progress: unit test Signed-off-by: deepanshu --- ros2service/test/fixtures/echo_client.py | 49 ++++++++++++++++ ros2service/test/test_cli.py | 72 +++++++++++++++++------- 2 files changed, 102 insertions(+), 19 deletions(-) diff --git a/ros2service/test/fixtures/echo_client.py b/ros2service/test/fixtures/echo_client.py index e69de29bb..63a0e58e4 100644 --- a/ros2service/test/fixtures/echo_client.py +++ b/ros2service/test/fixtures/echo_client.py @@ -0,0 +1,49 @@ +# Copyright 2022 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import rclpy +from rclpy.node import Node + +from test_msgs.srv import BasicTypes + + +class EchoClient(Node): + + def __init__(self): + super().__init__('echo_client') + self.future = None + self.client = self.create_client(BasicTypes, 'echo') + while not self.client.wait_for_service(timeout_sec=1.0): + self.get_logger().info('echo service not available, waiting again...') + self.req = BasicTypes.Request() + + def send_request(self): + print("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") + print("send_request") + self.req.string_value = "test" + self.future = self.client.call_async(self.req) + rclpy.spin_until_future_complete(self, self.future) + return self.future.result() + + +def main(args=None): + rclpy.init(args=args) + node = EchoClient() + node.send_request() + node.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/ros2service/test/test_cli.py b/ros2service/test/test_cli.py index 67967472e..0de23f1e1 100644 --- a/ros2service/test/test_cli.py +++ b/ros2service/test/test_cli.py @@ -65,10 +65,29 @@ def get_echo_call_output(**kwargs): @pytest.mark.rostest @launch_testing.parametrize('rmw_implementation', get_available_rmw_implementations()) def generate_test_description(rmw_implementation): - path_to_echo_server_script = os.path.join( - os.path.dirname(__file__), 'fixtures', 'echo_server.py' + path_to_fixtures = os.path.join(os.path.dirname(__file__), 'fixtures') + path_to_echo_server_script = os.path.join(path_to_fixtures, 'echo_server.py') + path_to_echo_client_script = os.path.join(path_to_fixtures, 'echo_client.py') + + additional_env = {'RMW_IMPLEMENTATION': rmw_implementation, 'PYTHONUNBUFFERED': '1'} + + echo_service_server_action = Node( + executable=sys.executable, + arguments=[path_to_echo_server_script], + name='echo_server', + namespace='my_ns', + additional_env=additional_env + ) + + echo_hidden_service_server_action = Node( + executable=sys.executable, + arguments=[path_to_echo_server_script], + name='_hidden_echo_server', + namespace='my_ns', + remappings=[('echo', '_echo')], + additional_env=additional_env, ) - additional_env = {'RMW_IMPLEMENTATION': rmw_implementation} + return LaunchDescription([ # Always restart daemon to isolate tests. ExecuteProcess( @@ -80,28 +99,15 @@ def generate_test_description(rmw_implementation): name='daemon-start', on_exit=[ # Add test fixture actions. - Node( - executable=sys.executable, - arguments=[path_to_echo_server_script], - name='echo_server', - namespace='my_ns', - additional_env=additional_env, - ), - Node( - executable=sys.executable, - arguments=[path_to_echo_server_script], - name='_hidden_echo_server', - namespace='my_ns', - remappings=[('echo', '_echo')], - additional_env=additional_env, - ), + echo_service_server_action, + echo_hidden_service_server_action, launch_testing.actions.ReadyToTest() ], additional_env=additional_env ) ] ), - ]) + ]), locals() class TestROS2ServiceCLI(unittest.TestCase): @@ -138,6 +144,34 @@ def launch_service_command(self, arguments): yield service_command cls.launch_service_command = launch_service_command + def test_service_echo(self, launch_service, proc_info, proc_output, path_to_echo_client_script, additional_env): + echo_service_client_action = Node( + executable=sys.executable, + arguments=[path_to_echo_client_script], + name='echo_client', + namespace='my_ns', + additional_env=additional_env + ) + + with self.launch_service_command( + arguments=['echo', '/my_ns/echo'] + ) as service_command: + with launch_testing.tools.launch_process( + launch_service, echo_service_client_action, + proc_info, proc_output + ) as client_node: + assert client_node.wait_for_shutdown(10) + assert client_node.exit_code == launch_testing.asserts.EXIT_OK + + print("") + print("") + print("############### HERE START #####################") + service_command.wait_for_output(timeout=10) + print(service_command.output) + print("") + # assert service_command.wait_for_shutdown(timeout=10) + print("############### HERE END #######################") + @launch_testing.markers.retry_on_failure(times=5, delay=1) def test_list_services(self): with self.launch_service_command(arguments=['list']) as service_command: From 86596927849e4b10b5f973064e2beba52769eb70 Mon Sep 17 00:00:00 2001 From: deepanshu Date: Tue, 19 Jul 2022 17:35:24 -0400 Subject: [PATCH 17/19] work in progress: unit tests Signed-off-by: deepanshu --- ros2interface/test/test_cli.py | 3 +-- ros2service/test/test_cli.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/ros2interface/test/test_cli.py b/ros2interface/test/test_cli.py index 6bd2bf1ca..8f1c91c5b 100644 --- a/ros2interface/test/test_cli.py +++ b/ros2interface/test/test_cli.py @@ -132,8 +132,7 @@ def test_list_messages(self): strict=False ) - def \ - test_list_services(self): + def test_list_services(self): with self.launch_interface_command(arguments=['list', '-s']) as interface_command: assert interface_command.wait_for_shutdown(timeout=2) assert interface_command.exit_code == launch_testing.asserts.EXIT_OK diff --git a/ros2service/test/test_cli.py b/ros2service/test/test_cli.py index 0de23f1e1..f5d3ec1a4 100644 --- a/ros2service/test/test_cli.py +++ b/ros2service/test/test_cli.py @@ -69,7 +69,7 @@ def generate_test_description(rmw_implementation): path_to_echo_server_script = os.path.join(path_to_fixtures, 'echo_server.py') path_to_echo_client_script = os.path.join(path_to_fixtures, 'echo_client.py') - additional_env = {'RMW_IMPLEMENTATION': rmw_implementation, 'PYTHONUNBUFFERED': '1'} + additional_env = {'RMW_IMPLEMENTATION': rmw_implementation} echo_service_server_action = Node( executable=sys.executable, @@ -161,16 +161,13 @@ def test_service_echo(self, launch_service, proc_info, proc_output, path_to_echo proc_info, proc_output ) as client_node: assert client_node.wait_for_shutdown(10) - assert client_node.exit_code == launch_testing.asserts.EXIT_OK - print("") - print("") - print("############### HERE START #####################") - service_command.wait_for_output(timeout=10) - print(service_command.output) - print("") - # assert service_command.wait_for_shutdown(timeout=10) - print("############### HERE END #######################") + @launch_testing.markers.retry_on_failure(times=5, delay=1) + def test_echo_service_not_running(self): + with self.launch_service_command( + arguments=['echo', '/service_not_running'] + ) as service_command: + assert service_command.wait_for_output(timeout=20) @launch_testing.markers.retry_on_failure(times=5, delay=1) def test_list_services(self): @@ -249,6 +246,7 @@ def test_find(self): strict=True ) + @launch_testing.markers.retry_on_failure(times=5, delay=1) def test_find_hidden(self): with self.launch_service_command( From d1c1ab435900e213841041d834927bd251418528 Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Fri, 5 Aug 2022 17:03:36 -0700 Subject: [PATCH 18/19] non-serialized Signed-off-by: Brian Chen --- ros2service/ros2service/verb/echo.py | 204 +++++++++++---------------- ros2topic/ros2topic/api/__init__.py | 5 +- 2 files changed, 85 insertions(+), 124 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 022183450..e2b75a9cb 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -11,40 +11,35 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import sys -from typing import Optional -from typing import TypeVar - from collections import OrderedDict +import sys +from typing import Optional, TypeVar import rclpy -from rclpy.node import Node +from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy from rclpy.qos import QoSPresetProfiles +from rosidl_runtime_py import message_to_csv +from rosidl_runtime_py import message_to_ordereddict +from rosidl_runtime_py.utilities import get_service +from service_msgs.msg import ServiceEventInfo +import yaml from ros2cli.node.strategy import NodeStrategy -from ros2topic.api import unsigned_int -from rcl_interfaces.msg import ServiceEventType - -from ros2service.api import ServiceNameCompleter, get_service_class +from ros2service.api import get_service_class +from ros2service.api import ServiceNameCompleter from ros2service.api import ServiceTypeCompleter from ros2service.verb import VerbExtension - -from rosidl_runtime_py import message_to_csv, message_to_ordereddict -from rosidl_runtime_py.utilities import get_message, get_service - -import yaml +from ros2topic.api import unsigned_int DEFAULT_TRUNCATE_LENGTH = 128 MsgType = TypeVar('MsgType') - def represent_ordereddict(dumper, data): items = [] for k, v in data.items(): items.append((dumper.represent_data(k), dumper.represent_data(v))) return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', items) - class EchoVerb(VerbExtension): """Echo a service.""" @@ -54,15 +49,15 @@ def __init__(self): self.no_arr = None self.csv = None self.flow_style = None - self.client = None - self.server = None + self.client_only = None + self.server_only = None self.truncate_length = None - self.include_message_info = None + self.exclude_message_info = None self.srv_module = None self.event_enum = None - self.event_msg_type = get_message("rcl_interfaces/msg/ServiceEvent") self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") self.__yaml_representer_registered = False + self.event_type_map = dict((v, k) for k, v in ServiceEventInfo._Metaclass_ServiceEventInfo__constants.items()) def add_arguments(self, parser, cli_name): arg = parser.add_argument( @@ -91,146 +86,111 @@ def add_arguments(self, parser, cli_name): parser.add_argument( '--csv', action='store_true', help=( - 'Output all recursive fields separated by commas (e.g. for ' - 'plotting). ' - 'If --include-message-info is also passed, the following fields are prepended: ' - 'source_timestamp, received_timestamp, publication_sequence_number,' - ' reception_sequence_number.' - ) - ) + 'Output all recursive fields separated by commas (e.g. for plotting).' + )) parser.add_argument( - '--include-message-info', '-i', action='store_true', - help='Shows the associated message info.') + '--exclude-message-info', action='store_true', help='Hide associated message info.') parser.add_argument( - '--client', action='store_true', help="Echo only request sent or response received by service client") + '--client-only', action='store_true', help="Echo only request sent or response received by service client") parser.add_argument( - '--server', action='store_true', help="Echo only request received or response sent by service server") + '--server-only', action='store_true', help="Echo only request received or response sent by service server") def main(self, *, args): self.truncate_length = args.truncate_length if not args.full_length else None self.no_arr = args.no_arr self.no_str = args.no_str self.csv = args.csv - self.include_message_info = args.include_message_info - self.client = args.client - self.server = args.server + self.exclude_message_info = args.exclude_message_info + self.client_only = args.client_only + self.server_only = args.server_only + event_topic_name = args.service_name + \ + _rclpy.service_introspection.RCL_SERVICE_INTROSPECTION_TOPIC_POSTFIX + + if self.server_only and self.client_only: + raise RuntimeError("--client-only and --server-only are mutually exclusive") if args.service_type is None: with NodeStrategy(args) as node: - self.srv_module = get_service_class( - node, args.service_name, blocking=False, include_hidden_services=True) + try: + self.srv_module = get_service_class( + node, args.service_name, blocking=False, include_hidden_services=True) + self.event_msg_type = self.srv_module.Event + except (AttributeError, ModuleNotFoundError, ValueError): + raise RuntimeError("The service name '%s' is invalid" % args.service_name) else: try: self.srv_module = get_service(args.service_type) + self.event_msg_type = self.srv_module.Event except (AttributeError, ModuleNotFoundError, ValueError): raise RuntimeError("The service type '%s' is invalid" % args.service_type) if self.srv_module is None: - raise RuntimeError( - 'Could not load the type for the passed service') - - event_topic_name = args.service_name + "/_service_event" + raise RuntimeError('Could not load the type for the passed service') with NodeStrategy(args) as node: self.subscribe_and_spin( node, event_topic_name, - self.event_msg_type - ) - - def subscribe_and_spin( - self, - node: Node, - event_topic_name: str, - event_msg_type: MsgType, - ) -> Optional[str]: - """Initialize a node with a single subscription and spin.""" + self.event_msg_type) + def subscribe_and_spin(self, node, event_topic_name: str, event_msg_type: MsgType) -> Optional[str]: + """Initialize a node with a single subscription and spin.""" node.create_subscription( event_msg_type, event_topic_name, self._subscriber_callback, self.qos_profile) - rclpy.spin(node) - def _subscriber_callback(self, msg, info): - deserialize_msg_type = None - self.event_enum = msg.info.event_type - - if self.client and self.server: - pass - elif self.client: - if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ - self.event_enum is ServiceEventType.RESPONSE_SENT: - return - elif self.server: - if self.event_enum is ServiceEventType.REQUEST_SENT or \ - self.event_enum is ServiceEventType.RESPONSE_RECEIVED: - return - - if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ - self.event_enum is ServiceEventType.REQUEST_SENT: - deserialize_msg_type = self.srv_module.Request - elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED or \ - self.event_enum is ServiceEventType.RESPONSE_SENT: - deserialize_msg_type = self.srv_module.Response - else: # TODO remove this once event enum is correct - print("Returning invalid service event type") - return - - # csv + def _subscriber_callback(self, msg): if self.csv: - to_print = message_to_csv( - msg, - truncate_length=self.truncate_length, - no_arr=self.no_arr, - no_str=self.no_str, - deserialize_msg_type=deserialize_msg_type - ) - if self.include_message_info: - to_print = f'{",".join(str(x) for x in info.values())},{to_print}' - print(to_print) - return - - # yaml - if self.include_message_info: - print(yaml.dump(info), end='---\n') - print( - self.format_output(message_to_ordereddict( - msg, truncate_length=self.truncate_length, - no_arr=self.no_arr, no_str=self.no_str, deserialize_msg_type=deserialize_msg_type)), - end='---------------------------\n') - - def format_output(self, dict_service_event: OrderedDict): - dict_output = OrderedDict() - dict_output['stamp'] = dict_service_event['info']['stamp'] - dict_output['client_id'] = dict_service_event['info']['client_id'] - dict_output['sequence_number'] = dict_service_event['info']['sequence_number'] - - if self.event_enum is ServiceEventType.REQUEST_SENT: - dict_output['event_type'] = 'CLIENT_REQUEST_SENT' - elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED: - dict_output['event_type'] = 'CLIENT_RESPONSE_RECEIVED' - elif self.event_enum is ServiceEventType.REQUEST_RECEIVED: - dict_output['event_type'] = 'SERVER_REQUEST_RECEIVED' - elif self.event_enum is ServiceEventType.RESPONSE_SENT: - dict_output['event_type'] = 'SERVER_RESPONSE_SENT' - - if self.event_enum is ServiceEventType.REQUEST_RECEIVED or \ - self.event_enum is ServiceEventType.REQUEST_SENT: - dict_output['request'] = dict_service_event['serialized_event'] - elif self.event_enum is ServiceEventType.RESPONSE_RECEIVED or \ - self.event_enum is ServiceEventType.RESPONSE_SENT: - dict_output['response'] = dict_service_event['serialized_event'] + print(self.format_csv_output(msg)) + else: + print(self.format_yaml_output(msg)) + print('---------------------------') + + def format_csv_output(self, msg: MsgType): + """Convert a message to a CSV string.""" + if self.exclude_message_info: + msg.info = ServiceEventInfo() + to_print = message_to_csv( + msg, + truncate_length=self.truncate_length, + no_arr=self.no_arr, + no_str=self.no_str) + return to_print + + def format_yaml_output(self, msg: MsgType): + """Pretty-format a service event message.""" + event_dict = message_to_ordereddict( + msg, + truncate_length=self.truncate_length, + no_arr=self.no_arr, + no_str=self.no_str) + + event_dict['info']['event_type'] = \ + self.event_type_map[event_dict['info']['event_type']] + + if self.exclude_message_info: + del event_dict['info'] + + # unpack Request, Response sequences + if len(event_dict['request']) == 0: + del event_dict['request'] + else: + event_dict['request'] = event_dict['request'][0] + + if len(event_dict['response']) == 0: + del event_dict['response'] + else: + event_dict['response'] = event_dict['response'][0] # Register custom representer for YAML output if not self.__yaml_representer_registered: yaml.add_representer(OrderedDict, represent_ordereddict) self.__yaml_representer_registered = True - return yaml.dump(dict_output, + return yaml.dump(event_dict, allow_unicode=True, width=sys.maxsize, - default_flow_style=self.flow_style, - ) + default_flow_style=self.flow_style) diff --git a/ros2topic/ros2topic/api/__init__.py b/ros2topic/ros2topic/api/__init__.py index c9688071c..f05237504 100644 --- a/ros2topic/ros2topic/api/__init__.py +++ b/ros2topic/ros2topic/api/__init__.py @@ -142,8 +142,9 @@ def _get_msg_class(node, topic, include_hidden_topics): try: return get_message(message_type) - except (AttributeError, ModuleNotFoundError, ValueError): - raise RuntimeError("The message type '%s' is invalid" % message_type) + except (AttributeError, ModuleNotFoundError, ValueError) as e: + raise e + # raise RuntimeError("The message type '%s' is invalid" % message_type) class TopicMessagePrototypeCompleter: From 7f51e5be0d8844234c9baa11e5d2a13a4c903d94 Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Fri, 5 Aug 2022 21:37:01 -0700 Subject: [PATCH 19/19] nicer uuid formatting Signed-off-by: Brian Chen --- ros2service/ros2service/verb/echo.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ros2service/ros2service/verb/echo.py b/ros2service/ros2service/verb/echo.py index 450c72f29..ef223a0b0 100644 --- a/ros2service/ros2service/verb/echo.py +++ b/ros2service/ros2service/verb/echo.py @@ -18,6 +18,7 @@ import sys from typing import Optional, TypeVar +import uuid import rclpy from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy from rclpy.qos import QoSPresetProfiles @@ -60,7 +61,8 @@ def __init__(self): self.event_enum = None self.qos_profile = QoSPresetProfiles.get_from_short_key("services_default") self.__yaml_representer_registered = False - self.event_type_map = dict((v, k) for k, v in ServiceEventInfo._Metaclass_ServiceEventInfo__constants.items()) + self.event_type_map = dict( + (v, k) for k, v in ServiceEventInfo._Metaclass_ServiceEventInfo__constants.items()) def add_arguments(self, parser, cli_name): arg = parser.add_argument( @@ -93,9 +95,12 @@ def add_arguments(self, parser, cli_name): parser.add_argument( '--exclude-message-info', action='store_true', help='Hide associated message info.') parser.add_argument( - '--client-only', action='store_true', help="Echo only request sent or response received by service client") + '--client-only', action='store_true', help='Echo only request sent or response received by service client') parser.add_argument( - '--server-only', action='store_true', help="Echo only request received or response sent by service server") + '--server-only', action='store_true', help='Echo only request received or response sent by service server') + parser.add_argument( + '--uuid-list', + action='store_true', help='Print client_id as uint8 list UUID instead of string UUID') def main(self, *, args): self.truncate_length = args.truncate_length if not args.full_length else None @@ -105,6 +110,7 @@ def main(self, *, args): self.exclude_message_info = args.exclude_message_info self.client_only = args.client_only self.server_only = args.server_only + self.uuid_list = args.uuid_list event_topic_name = args.service_name + \ _rclpy.service_introspection.RCL_SERVICE_INTROSPECTION_TOPIC_POSTFIX @@ -172,6 +178,10 @@ def format_yaml_output(self, msg: MsgType): event_dict['info']['event_type'] = \ self.event_type_map[event_dict['info']['event_type']] + + if not self.uuid_list: + uuid_hex_str = "".join([f'{i:02x}' for i in event_dict['info']['client_id']['uuid']]) + event_dict['info']['client_id']['uuid'] = str(uuid.UUID(uuid_hex_str)) if self.exclude_message_info: del event_dict['info']