-
Notifications
You must be signed in to change notification settings - Fork 205
[service introspection] ros2 service echo #745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+490
−11
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ac463d9
Implement ros2 service echo.
deepanshubansal01 a06f60f
Add test for service echo verb
jacobperron fc5b221
Fixes from review.
clalancette 6458a8d
Fixes from review.
clalancette ba9cedf
Switch to meaningful names for the event_type.
clalancette File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| # 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. | ||
|
|
||
| from collections import OrderedDict | ||
| import sys | ||
| from typing import TypeVar | ||
|
|
||
| import rclpy | ||
|
|
||
| from rclpy.qos import QoSPresetProfiles | ||
| from ros2cli.helpers import unsigned_int | ||
| from ros2cli.node.strategy import NodeStrategy | ||
| 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 | ||
| from rosidl_runtime_py import message_to_ordereddict | ||
| from rosidl_runtime_py.utilities import get_service | ||
| from service_msgs.msg import ServiceEventInfo | ||
|
|
||
| import yaml | ||
|
|
||
|
|
||
| DEFAULT_TRUNCATE_LENGTH = 128 | ||
| MsgType = TypeVar('MsgType') | ||
|
|
||
|
|
||
| class EchoVerb(VerbExtension): | ||
| """Echo a service.""" | ||
|
|
||
| # Custom representer for getting clean YAML output that preserves the order in an OrderedDict. | ||
| # Inspired by: http://stackoverflow.com/a/16782282/7169408 | ||
| def __represent_ordereddict(self, 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) | ||
|
|
||
| def __init__(self): | ||
| self._event_number_to_name = {} | ||
| for k, v in ServiceEventInfo._Metaclass_ServiceEventInfo__constants.items(): | ||
| self._event_number_to_name[v] = k | ||
|
|
||
| yaml.add_representer(OrderedDict, self.__represent_ordereddict) | ||
|
|
||
| def add_arguments(self, parser, cli_name): | ||
| arg = parser.add_argument( | ||
| 'service_name', | ||
| 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. 'example_interfaces/srv/AddTwoInts')") | ||
| arg.completer = ServiceTypeCompleter(service_name_key='service_name') | ||
| parser.add_argument( | ||
| '--csv', action='store_true', default=False, | ||
| help=( | ||
| 'Output all recursive fields separated by commas (e.g. for plotting).' | ||
| )) | ||
| 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( | ||
| '--flow-style', action='store_true', | ||
| help='Print collections in the block style (not available with csv format)') | ||
|
|
||
| def main(self, *, args): | ||
| if args.service_type is None: | ||
| with NodeStrategy(args) as node: | ||
| try: | ||
| srv_module = get_service_class( | ||
| node, args.service_name, include_hidden_services=True) | ||
| except (AttributeError, ModuleNotFoundError, ValueError): | ||
| raise RuntimeError(f"The service name '{args.service_name}' is invalid") | ||
| else: | ||
| try: | ||
| srv_module = get_service(args.service_type) | ||
| except (AttributeError, ModuleNotFoundError, ValueError): | ||
| raise RuntimeError(f"The service type '{args.service_type}' is invalid") | ||
|
|
||
| if srv_module is None: | ||
| raise RuntimeError('Could not load the type for the passed service') | ||
|
|
||
| event_msg_type = srv_module.Event | ||
|
|
||
| # TODO(clalancette): We should probably expose this postfix from rclpy | ||
| event_topic_name = args.service_name + '/_service_event' | ||
|
|
||
| self.csv = args.csv | ||
| self.truncate_length = args.truncate_length if not args.full_length else None | ||
| self.flow_style = args.flow_style | ||
| self.no_arr = args.no_arr | ||
| self.no_str = args.no_str | ||
|
|
||
| with NodeStrategy(args) as node: | ||
| sub = node.create_subscription( | ||
| event_msg_type, | ||
| event_topic_name, | ||
| self._subscriber_callback, | ||
| QoSPresetProfiles.get_from_short_key('services_default')) | ||
|
|
||
| have_printed_warning = False | ||
| executor = rclpy.get_global_executor() | ||
| try: | ||
| executor.add_node(node) | ||
| while executor.context.ok(): | ||
| if not have_printed_warning and sub.get_publisher_count() < 1: | ||
| print(f"No publishers on topic '{event_topic_name}'; " | ||
| 'is service introspection on the client or server enabled?') | ||
| have_printed_warning = True | ||
| executor.spin_once() | ||
| finally: | ||
| executor.remove_node(node) | ||
|
|
||
| sub.destroy() | ||
|
|
||
| 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) | ||
| else: | ||
| # The "easy" way to print out a representation here is to call message_to_yaml(). | ||
| # However, the message contains numbers for the event type, but we want to show | ||
| # meaningful names to the user. So we call message_to_ordereddict() instead, | ||
| # and replace the numbers with meaningful names before dumping to YAML. | ||
| msgdict = message_to_ordereddict(msg, truncate_length=self.truncate_length, | ||
| no_arr=self.no_arr, no_str=self.no_str) | ||
|
|
||
| if 'info' in msgdict: | ||
| info = msgdict['info'] | ||
| if 'event_type' in info: | ||
| info['event_type'] = self._event_number_to_name[info['event_type']] | ||
|
|
||
| to_print = yaml.dump(msgdict, allow_unicode=True, width=sys.maxsize, | ||
| default_flow_style=self.flow_style) | ||
|
|
||
| to_print += '---' | ||
|
|
||
| print(to_print) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # Copyright 2023 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.executors import ExternalShutdownException | ||
| from rclpy.executors import SingleThreadedExecutor | ||
| from rclpy.node import Node | ||
| from rclpy.qos import qos_profile_system_default | ||
| from rclpy.service_introspection import ServiceIntrospectionState | ||
|
|
||
| from test_msgs.srv import BasicTypes | ||
|
|
||
|
|
||
| class IntrospectableService(Node): | ||
|
|
||
| def __init__(self): | ||
| super().__init__('introspectable_service') | ||
| self.service = self.create_service(BasicTypes, 'test_introspectable', self.callback) | ||
| self.service.configure_introspection( | ||
| self.get_clock(), qos_profile_system_default, ServiceIntrospectionState.CONTENTS) | ||
|
|
||
| def callback(self, request, response): | ||
| for field_name in request.get_fields_and_field_types(): | ||
| setattr(response, field_name, getattr(request, field_name)) | ||
| return response | ||
|
|
||
|
|
||
| class IntrospectableClient(Node): | ||
|
|
||
| def __init__(self): | ||
| super().__init__('introspectable_client') | ||
| self.client = self.create_client(BasicTypes, 'test_introspectable') | ||
| self.client.configure_introspection( | ||
| self.get_clock(), qos_profile_system_default, ServiceIntrospectionState.CONTENTS) | ||
|
|
||
| self.timer = self.create_timer(0.1, self.timer_callback) | ||
| self.future = None | ||
|
|
||
| def timer_callback(self): | ||
| if not self.client.service_is_ready(): | ||
| return | ||
|
|
||
| if self.future is None: | ||
| request = BasicTypes.Request() | ||
| request.bool_value = True | ||
| request.int32_value = 42 | ||
| request.string_value = 'test_string_value' | ||
| self.future = self.client.call_async(request) | ||
| return | ||
|
|
||
| if not self.future.done(): | ||
| return | ||
|
|
||
| if self.future.result() is None: | ||
| self.get_logger().error(f'Exception calling service: {self.future.exception()!r}') | ||
|
|
||
| self.future = None | ||
|
|
||
|
|
||
| def main(args=None): | ||
| rclpy.init(args=args) | ||
|
|
||
| service_node = IntrospectableService() | ||
| client_node = IntrospectableClient() | ||
|
|
||
| executor = SingleThreadedExecutor() | ||
| executor.add_node(service_node) | ||
| executor.add_node(client_node) | ||
|
|
||
| try: | ||
| executor.spin() | ||
| except (KeyboardInterrupt, ExternalShutdownException): | ||
| executor.remove_node(client_node) | ||
| executor.remove_node(service_node) | ||
| executor.shutdown() | ||
| service_node.destroy_node() | ||
| client_node.destroy_node() | ||
| rclpy.try_shutdown() | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.