From 1caf6a2dd676589d96ec6293cce3e808da49ec61 Mon Sep 17 00:00:00 2001 From: Chris Lalancette Date: Thu, 23 Feb 2023 20:15:45 +0000 Subject: [PATCH 1/2] Add in a rclpy demo of introspection. Signed-off-by: Chris Lalancette --- .../demo_nodes_py/services/introspection.py | 211 ++++++++++++++++++ demo_nodes_py/package.xml | 1 + demo_nodes_py/setup.py | 3 +- 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 demo_nodes_py/demo_nodes_py/services/introspection.py diff --git a/demo_nodes_py/demo_nodes_py/services/introspection.py b/demo_nodes_py/demo_nodes_py/services/introspection.py new file mode 100644 index 000000000..7718254c2 --- /dev/null +++ b/demo_nodes_py/demo_nodes_py/services/introspection.py @@ -0,0 +1,211 @@ +# 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. + +from example_interfaces.srv import AddTwoInts +from rcl_interfaces.msg import SetParametersResult + +import rclpy +from rclpy.executors import ExternalShutdownException +from rclpy.executors import SingleThreadedExecutor +from rclpy.node import Node +from rclpy.parameter import Parameter +from rclpy.qos import qos_profile_system_default +from rclpy.service_introspection import ServiceIntrospectionState + + +# This demo program shows how to configure client and service introspection +# on the fly, by hooking it up to a parameter. This program consists of both +# a client node (IntrospectionClientNode) and a service node (IntrospectionServiceNode). +# At startup time, one of each type of node is created, and added to an executor. +# The IntrospectionServiceNode listens on the '/add_two_ints' service for clients; +# when one connects and sends a request, it adds the two integers and returns the result. +# The IntrospectionClientNode is slightly more complicated. It has a timer callback that +# runs every 500 milliseconds. If the service is not yet ready, no further work +# is done. If the client doesn't currently have a future outstanding, then it +# creates a new AddTwoInts service request, and asynchronously sends it to the service. +# If the client does have a future outstanding, then we check if it is done. +# Once the future has been completed, we forget about the future so we can send +# another request. +# +# The above is a fairly common ROS 2 client and service, but what this program +# is trying to demonstrate is introspection capabilities. +# The IntrospectionClientNode has a string parameter called 'client_configure_introspection', +# and the IntrospectionServiceNode has a string parameter called 'service_configure_introspection'. +# If these are set to 'off' (the default), then no introspection happens. +# If these are set to 'metadata' (see details on how to set the parameters below), +# then essential metadata (timestamps, sequence numbers, etc) are sent to a +# hidden topic called /add_two_ints/_service_event. +# +# To see this in action, this program can be run in a couple of different ways: +# +# ros2 run demo_nodes_py introspection +# Since the default for introspection is 'off', this is no different than +# a normal client and server. No additional topics will be made, and +# no introspection data will be sent. However, changing the introspection +# configuration dynamically is fully supported. This can be seen by +# running 'ros2 param set /introspection_client client_configure_introspection metadata' +# which will configure the client to start sending service introspection +# metadata to /add_two_ints/_service_event. +# +# ros2 run demo_nodes_py introspection \ +# --ros-args -p client_configure_introspection:=metadata \ +# -p service_configure_introspection:=metadata +# Here we've set both the client and service introspection to metadata, +# so the /add_two_ints/_service_event will be created. Additionally, +# every client request and response and every service acceptance and +# response will be sent to that topic. +# +# In either case, service introspection data can be seen by running: +# ros2 topic echo /add_two_ints/_service_event + +def check_parameter(parameter_list, parameter_name): + result = SetParametersResult() + result.successful = True + for param in parameter_list: + if param.name != parameter_name: + continue + + if param.type_ != Parameter.Type.STRING: + result.successful = False + result.reason = 'must be a string' + break + + if param.value not in ('off', 'metadata', 'contents'): + result.successful = False + result.reason = "must be one of 'off', 'metadata', or 'contents" + break + + return result + + +class IntrospectionClientNode(Node): + + def on_set_parameters_callback(self, parameter_list): + return check_parameter(parameter_list, 'client_configure_introspection') + + def on_post_set_parameters_callback(self, parameter_list): + for param in parameter_list: + if param.name != 'client_configure_introspection': + continue + + introspection_state = ServiceIntrospectionState.OFF + if param.value == 'off': + introspection_state = ServiceIntrospectionState.OFF + elif param.value == 'metadata': + introspection_state = ServiceIntrospectionState.METADATA + elif param.value == 'contents': + introspection_state = ServiceIntrospectionState.CONTENTS + + self.cli.configure_introspection(self.get_clock(), qos_profile_system_default, + introspection_state) + break + + def __init__(self): + super().__init__('introspection_client') + + self.cli = self.create_client(AddTwoInts, 'add_two_ints') + + self.add_on_set_parameters_callback(self.on_set_parameters_callback) + self.add_post_set_parameters_callback(self.on_post_set_parameters_callback) + self.declare_parameter('client_configure_introspection', 'off') + + self.timer = self.create_timer(0.5, self.timer_callback) + self.future = None + + def timer_callback(self): + if not self.cli.service_is_ready(): + return + + if self.future is None: + req = AddTwoInts.Request() + req.a = 2 + req.b = 3 + + self.future = self.cli.call_async(req) + + return + + if not self.future.done(): + return + + if self.future.result() is not None: + self.get_logger().info('Result of add_two_ints: %d' % self.future.result().sum) + else: + self.get_logger().error('Exception calling service: %r' % self.future.exception()) + + self.future = None + + +class IntrospectionServiceNode(Node): + + def on_set_parameters_callback(self, parameter_list): + return check_parameter(parameter_list, 'service_configure_introspection') + + def on_post_set_parameters_callback(self, parameter_list): + for param in parameter_list: + if param.name != 'service_configure_introspection': + continue + + introspection_state = ServiceIntrospectionState.OFF + if param.value == 'off': + introspection_state = ServiceIntrospectionState.OFF + elif param.value == 'metadata': + introspection_state = ServiceIntrospectionState.METADATA + elif param.value == 'contents': + introspection_state = ServiceIntrospectionState.CONTENTS + + self.srv.configure_introspection(self.get_clock(), qos_profile_system_default, + introspection_state) + break + + def __init__(self): + super().__init__('introspection_service') + + self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback) + + self.add_on_set_parameters_callback(self.on_set_parameters_callback) + self.add_post_set_parameters_callback(self.on_post_set_parameters_callback) + self.declare_parameter('service_configure_introspection', 'off') + + def add_two_ints_callback(self, request, response): + response.sum = request.a + request.b + self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b)) + + return response + + +def main(args=None): + rclpy.init(args=args) + + service_node = IntrospectionServiceNode() + + client_node = IntrospectionClientNode() + + 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() diff --git a/demo_nodes_py/package.xml b/demo_nodes_py/package.xml index 3c22ff438..482661451 100644 --- a/demo_nodes_py/package.xml +++ b/demo_nodes_py/package.xml @@ -20,6 +20,7 @@ example_interfaces rclpy + rcl_interfaces std_msgs ament_copyright diff --git a/demo_nodes_py/setup.py b/demo_nodes_py/setup.py index c505db55b..afbd9af0a 100644 --- a/demo_nodes_py/setup.py +++ b/demo_nodes_py/setup.py @@ -41,7 +41,8 @@ 'add_two_ints_client_async = demo_nodes_py.services.add_two_ints_client_async:main', 'add_two_ints_server = demo_nodes_py.services.add_two_ints_server:main', 'async_param_client = demo_nodes_py.parameters.async_param_client:main', - 'set_parameters_callback = demo_nodes_py.parameters.set_parameters_callback:main' + 'set_parameters_callback = demo_nodes_py.parameters.set_parameters_callback:main', + 'introspection = demo_nodes_py.services.introspection:main', ], }, ) From 699760bf338d274e16ed8da5f6193f9d4664e91d Mon Sep 17 00:00:00 2001 From: Chris Lalancette Date: Thu, 23 Feb 2023 21:37:15 +0000 Subject: [PATCH 2/2] Add in an rclcpp demo of introspection. Signed-off-by: Chris Lalancette --- demo_nodes_cpp/CMakeLists.txt | 10 +- .../services/introspect_services.launch.py | 29 +++ demo_nodes_cpp/package.xml | 2 + .../src/services/introspection_client.cpp | 166 ++++++++++++++++++ .../src/services/introspection_service.cpp | 150 ++++++++++++++++ 5 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 demo_nodes_cpp/launch/services/introspect_services.launch.py create mode 100644 demo_nodes_cpp/src/services/introspection_client.cpp create mode 100644 demo_nodes_cpp/src/services/introspection_service.cpp diff --git a/demo_nodes_cpp/CMakeLists.txt b/demo_nodes_cpp/CMakeLists.txt index 8e708ea75..8f405b2a5 100644 --- a/demo_nodes_cpp/CMakeLists.txt +++ b/demo_nodes_cpp/CMakeLists.txt @@ -61,7 +61,9 @@ add_library(timers_library SHARED add_library(services_library SHARED src/services/add_two_ints_server.cpp src/services/add_two_ints_client.cpp - src/services/add_two_ints_client_async.cpp) + src/services/add_two_ints_client_async.cpp + src/services/introspection_service.cpp + src/services/introspection_client.cpp) add_library(parameters_library SHARED src/parameters/list_parameters.cpp src/parameters/parameter_blackboard.cpp @@ -96,6 +98,12 @@ rclcpp_components_register_node(services_library rclcpp_components_register_node(services_library PLUGIN "demo_nodes_cpp::ClientNode" EXECUTABLE add_two_ints_client_async) +rclcpp_components_register_node(services_library + PLUGIN "demo_nodes_cpp::IntrospectionServiceNode" + EXECUTABLE introspection_service) +rclcpp_components_register_node(services_library + PLUGIN "demo_nodes_cpp::IntrospectionClientNode" + EXECUTABLE introspection_client) rclcpp_components_register_node(parameters_library PLUGIN "demo_nodes_cpp::ListParameters" diff --git a/demo_nodes_cpp/launch/services/introspect_services.launch.py b/demo_nodes_cpp/launch/services/introspect_services.launch.py new file mode 100644 index 000000000..25e55744b --- /dev/null +++ b/demo_nodes_cpp/launch/services/introspect_services.launch.py @@ -0,0 +1,29 @@ +# 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. + +"""Launch the introspection service and client.""" + +import launch +import launch_ros.actions + + +def generate_launch_description(): + server = launch_ros.actions.Node( + package='demo_nodes_cpp', executable='introspection_service', output='screen') + client = launch_ros.actions.Node( + package='demo_nodes_cpp', executable='introspection_client', output='screen') + return launch.LaunchDescription([ + server, + client, + ]) diff --git a/demo_nodes_cpp/package.xml b/demo_nodes_cpp/package.xml index 481df7ae1..d09560132 100644 --- a/demo_nodes_cpp/package.xml +++ b/demo_nodes_cpp/package.xml @@ -21,6 +21,7 @@ example_interfaces rclcpp rclcpp_components + rcl_interfaces rcutils rmw rmw_implementation_cmake @@ -31,6 +32,7 @@ launch_xml rclcpp rclcpp_components + rcl_interfaces rcutils rmw std_msgs diff --git a/demo_nodes_cpp/src/services/introspection_client.cpp b/demo_nodes_cpp/src/services/introspection_client.cpp new file mode 100644 index 000000000..ade6c9e1b --- /dev/null +++ b/demo_nodes_cpp/src/services/introspection_client.cpp @@ -0,0 +1,166 @@ +// 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. + +#include +#include +#include + +#include "rcl/service_introspection.h" + +#include "rclcpp/qos.hpp" +#include "rclcpp/rclcpp.hpp" +#include "rclcpp_components/register_node_macro.hpp" + +#include "example_interfaces/srv/add_two_ints.hpp" +#include "rcl_interfaces/msg/set_parameters_result.hpp" + +#include "demo_nodes_cpp/visibility_control.h" + +// This demo program shows how to configure client introspection on the fly, +// by hooking it up to a parameter. This program consists of a client +// node (IntrospectionClientNode) that has a timer callback that runs every +// 500 milliseconds. If the service is not yet ready, no further work is done. +// If the client doesn't currently have a request in flight, then it creates a +// new AddTwoInts service request, and asynchronously sends it to the service. +// When that request completes, it sets the flag back to having no requests in +// flight so another request is sent. +// +// The above is a fairly common ROS 2 client, but what this program is trying +// to demonstrate is introspection capabilities. The IntrospectionClientNode +// has a string parameter called 'client_configure_introspection'. If this is +// set to 'off' (the default), then no introspection happens. If this is set +// to 'metadata' (see details on how to set the parameters below), then +// essential metadata (timestamps, sequence numbers, etc) is sent to a hidden +// topic called /add_two_ints/_service_event. +// +// To see this in action, run the following: +// +// ros2 launch demo_nodes_cpp introspect_services.launch.py +// Since the default for introspection is 'off', this is no different than +// a normal client and server. No additional topics will be made, and +// no introspection data will be sent. However, changing the introspection +// configuration dynamically is fully supported. This can be seen by +// running 'ros2 param set /introspection_client client_configure_introspection metadata' +// which will configure the client to start sending service introspection +// metadata to /add_two_ints/_service_event. +// +// Once the parameter is set, introspection data can be seen by running: +// ros2 topic echo /add_two_ints/_service_event + +namespace demo_nodes_cpp +{ +class IntrospectionClientNode : public rclcpp::Node +{ +public: + DEMO_NODES_CPP_PUBLIC + explicit IntrospectionClientNode(const rclcpp::NodeOptions & options) + : Node("introspection_client", options) + { + client_ = create_client("add_two_ints"); + + auto on_set_parameter_callback = + [this](std::vector parameters) { + rcl_interfaces::msg::SetParametersResult result; + result.successful = true; + for (const rclcpp::Parameter & param : parameters) { + if (param.get_name() != "client_configure_introspection") { + continue; + } + + if (param.get_type() != rclcpp::ParameterType::PARAMETER_STRING) { + result.successful = false; + result.reason = "must be a string"; + break; + } + + if (param.as_string() != "off" && param.as_string() != "metadata" && + param.as_string() != "contents") + { + result.successful = false; + result.reason = "must be one of 'off', 'metadata', or 'contents'"; + break; + } + } + + return result; + }; + + auto post_set_parameter_callback = + [this](const std::vector & parameters) { + for (const rclcpp::Parameter & param : parameters) { + if (param.get_name() != "client_configure_introspection") { + continue; + } + + rcl_service_introspection_state_t introspection_state = RCL_SERVICE_INTROSPECTION_OFF; + + if (param.as_string() == "off") { + introspection_state = RCL_SERVICE_INTROSPECTION_OFF; + } else if (param.as_string() == "metadata") { + introspection_state = RCL_SERVICE_INTROSPECTION_METADATA; + } else if (param.as_string() == "contents") { + introspection_state = RCL_SERVICE_INTROSPECTION_CONTENTS; + } + + this->client_->configure_introspection( + this->get_clock(), rclcpp::SystemDefaultsQoS(), introspection_state); + break; + } + }; + + on_set_parameters_callback_handle_ = this->add_on_set_parameters_callback( + on_set_parameter_callback); + post_set_parameters_callback_handle_ = this->add_post_set_parameters_callback( + post_set_parameter_callback); + + this->declare_parameter("client_configure_introspection", "off"); + + timer_ = this->create_wall_timer( + std::chrono::milliseconds(500), + [this]() { + if (!client_->service_is_ready()) { + return; + } + + if (!request_in_progress_) { + auto request = std::make_shared(); + request->a = 2; + request->b = 3; + request_in_progress_ = true; + client_->async_send_request( + request, + [this](rclcpp::Client::SharedFuture cb_f) + { + request_in_progress_ = false; + RCLCPP_INFO(get_logger(), "Result of add_two_ints: %ld", cb_f.get()->sum); + } + ); + return; + } + }); + } + +private: + rclcpp::Client::SharedPtr client_; + rclcpp::TimerBase::SharedPtr timer_; + rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr + on_set_parameters_callback_handle_; + rclcpp::node_interfaces::PostSetParametersCallbackHandle::SharedPtr + post_set_parameters_callback_handle_; + bool request_in_progress_{false}; +}; + +} // namespace demo_nodes_cpp + +RCLCPP_COMPONENTS_REGISTER_NODE(demo_nodes_cpp::IntrospectionClientNode) diff --git a/demo_nodes_cpp/src/services/introspection_service.cpp b/demo_nodes_cpp/src/services/introspection_service.cpp new file mode 100644 index 000000000..62484b605 --- /dev/null +++ b/demo_nodes_cpp/src/services/introspection_service.cpp @@ -0,0 +1,150 @@ +// 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. + +#include +#include +#include + +#include "rcl/service_introspection.h" + +#include "rclcpp/qos.hpp" +#include "rclcpp/rclcpp.hpp" +#include "rclcpp_components/register_node_macro.hpp" + +#include "example_interfaces/srv/add_two_ints.hpp" +#include "rcl_interfaces/msg/set_parameters_result.hpp" + +#include "demo_nodes_cpp/visibility_control.h" + +// This demo program shows how to configure service introspection on the fly, +// by hooking it up to a parameter. This program consists of a service +// node (IntrospectionServiceNode) that listens on the '/add_two_ints' service +// for clients. When one connects and sends a request, it adds the two integers +// and returns the result. +// +// The above is a fairly common ROS 2 service, but what this program is trying +// to demonstrate is introspection capabilities. The IntrospectionServiceNode +// has a string parameter called 'service_configure_introspection'. If this is +// set to 'off' (the default), then no introspection happens. If this is set +// to 'metadata' (see details on how to set the parameters below), then +// essential metadata (timestamps, sequence numbers, etc) is sent to a hidden +// topic called /add_two_ints/_service_event. +// +// To see this in action, run the following: +// +// ros2 launch demo_nodes_cpp introspect_services.launch.py +// Since the default for introspection is 'off', this is no different than +// a normal client and server. No additional topics will be made, and +// no introspection data will be sent. However, changing the introspection +// configuration dynamically is fully supported. This can be seen by +// running 'ros2 param set /introspection_service service_configure_introspection metadata' +// which will configure the service to start sending service introspection +// metadata to /add_two_ints/_service_event. +// +// Once the parameter is set, introspection data can be seen by running: +// ros2 topic echo /add_two_ints/_service_event + +namespace demo_nodes_cpp +{ + +class IntrospectionServiceNode : public rclcpp::Node +{ +public: + DEMO_NODES_CPP_PUBLIC + explicit IntrospectionServiceNode(const rclcpp::NodeOptions & options) + : Node("introspection_service", options) + { + auto handle_add_two_ints = + [this](const std::shared_ptr request_header, + const std::shared_ptr request, + std::shared_ptr response) -> void + { + (void)request_header; + RCLCPP_INFO( + this->get_logger(), "Incoming request\na: %" PRId64 " b: %" PRId64, + request->a, request->b); + response->sum = request->a + request->b; + }; + // Create a service that will use the callback function to handle requests. + srv_ = create_service("add_two_ints", handle_add_two_ints); + + auto on_set_parameter_callback = + [this](std::vector parameters) { + rcl_interfaces::msg::SetParametersResult result; + result.successful = true; + for (const rclcpp::Parameter & param : parameters) { + if (param.get_name() != "service_configure_introspection") { + continue; + } + + if (param.get_type() != rclcpp::ParameterType::PARAMETER_STRING) { + result.successful = false; + result.reason = "must be a string"; + break; + } + + if (param.as_string() != "off" && param.as_string() != "metadata" && + param.as_string() != "contents") + { + result.successful = false; + result.reason = "must be one of 'off', 'metadata', or 'contents'"; + break; + } + } + + return result; + }; + + auto post_set_parameter_callback = + [this](const std::vector & parameters) { + for (const rclcpp::Parameter & param : parameters) { + if (param.get_name() != "service_configure_introspection") { + continue; + } + + rcl_service_introspection_state_t introspection_state = RCL_SERVICE_INTROSPECTION_OFF; + + if (param.as_string() == "off") { + introspection_state = RCL_SERVICE_INTROSPECTION_OFF; + } else if (param.as_string() == "metadata") { + introspection_state = RCL_SERVICE_INTROSPECTION_METADATA; + } else if (param.as_string() == "contents") { + introspection_state = RCL_SERVICE_INTROSPECTION_CONTENTS; + } + + this->srv_->configure_introspection( + this->get_clock(), rclcpp::SystemDefaultsQoS(), introspection_state); + break; + } + }; + + on_set_parameters_callback_handle_ = this->add_on_set_parameters_callback( + on_set_parameter_callback); + post_set_parameters_callback_handle_ = this->add_post_set_parameters_callback( + post_set_parameter_callback); + + this->declare_parameter("service_configure_introspection", "off"); + } + +private: + rclcpp::Service::SharedPtr srv_; + rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr + on_set_parameters_callback_handle_; + rclcpp::node_interfaces::PostSetParametersCallbackHandle::SharedPtr + post_set_parameters_callback_handle_; +}; + +} // namespace demo_nodes_cpp + +RCLCPP_COMPONENTS_REGISTER_NODE(demo_nodes_cpp::IntrospectionServiceNode)