AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
ros_service_node.hpp
1// Copyright 2024 Robin Müller
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#pragma once
16
17#include <chrono>
18#include <memory>
19#include <string>
20
21#include "auto_apms_behavior_tree_core/exceptions.hpp"
22#include "auto_apms_behavior_tree_core/node/base/ros_action_node_base.hpp"
23#include "auto_apms_util/logging.hpp"
24#include "rclcpp/executors.hpp"
25
27{
28
29enum ServiceNodeErrorCode
30{
31 SERVICE_UNREACHABLE,
32 SERVICE_TIMEOUT,
33 INVALID_REQUEST
34};
35
41inline const char * toStr(const ServiceNodeErrorCode & err)
42{
43 switch (err) {
44 case SERVICE_UNREACHABLE:
45 return "SERVICE_UNREACHABLE";
46 case SERVICE_TIMEOUT:
47 return "SERVICE_TIMEOUT";
48 case INVALID_REQUEST:
49 return "INVALID_REQUEST";
50 }
51 return nullptr;
52}
53
90template <class ServiceT>
91class RosServiceNode : public RosActionNodeBase
92{
93 using ServiceClient = typename rclcpp::Client<ServiceT>;
94 using ServiceClientPtr = std::shared_ptr<ServiceClient>;
95
96 struct ServiceClientInstance
97 {
98 ServiceClientInstance(
99 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & service_name);
100
101 ServiceClientPtr service_client;
102 std::string name;
103 };
104
105public:
106 using ServiceType = ServiceT;
107 using Request = typename ServiceT::Request;
108 using Response = typename ServiceT::Response;
109 using Config = BT::NodeConfig;
110 using Context = RosNodeContext;
111
121 explicit RosServiceNode(const std::string & instance_name, const Config & config, Context context);
122
123 virtual ~RosServiceNode() = default;
124
132 static BT::PortsList providedBasicPorts(BT::PortsList addition)
133 {
134 BT::PortsList basic = {BT::InputPort<std::string>("topic", "Name of the ROS 2 service.")};
135 basic.insert(addition.begin(), addition.end());
136 return basic;
137 }
138
143 static BT::PortsList providedPorts() { return providedBasicPorts({}); }
144
154 virtual bool setRequest(typename Request::SharedPtr & request);
155
165 virtual BT::NodeStatus onResponseReceived(const typename Response::SharedPtr & response);
166
176 virtual BT::NodeStatus onFailure(ServiceNodeErrorCode error);
177
183 bool createClient(const std::string & service_name);
184
189 std::string getServiceName() const;
190
191protected:
192 BT::NodeStatus tick() override final;
193
194 void halt() override final;
195
196private:
197 bool dynamic_client_instance_ = false;
198 std::shared_ptr<ServiceClientInstance> client_instance_;
199 typename ServiceClient::SharedFuture future_;
200 int64_t request_id_;
201 rclcpp::Time time_request_sent_;
202 BT::NodeStatus on_feedback_state_change_;
203 typename Response::SharedPtr response_;
204};
205
206// #####################################################################################################################
207// ################################ DEFINITIONS ##############################################
208// #####################################################################################################################
209
210template <class ServiceT>
211inline RosServiceNode<ServiceT>::ServiceClientInstance::ServiceClientInstance(
212 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & service_name)
213{
214 service_client = node->create_client<ServiceT>(service_name, rclcpp::ServicesQoS(), group);
215 name = service_name;
216}
217
218template <class ServiceT>
220 const std::string & instance_name, const Config & config, Context context)
221: RosActionNodeBase(instance_name, config, context)
222{
223 // The base class stores the context/logger and applies the node manifest 'port_alias' feature.
224
225 // Resolve topic field
226 if (const BT::Expected<std::string> expected_name = context_.getTopicName(this)) {
227 createClient(expected_name.value());
228 } else {
229 // We assume that determining the service name requires a blackboard pointer, which cannot be evaluated at
230 // construction time. The expression will be evaluated each time before the node is ticked the first time after
231 // successful execution.
232 dynamic_client_instance_ = true;
233 }
234}
235
236template <class ServiceT>
237inline BT::NodeStatus RosServiceNode<ServiceT>::tick()
238{
239 if (!rclcpp::ok()) {
240 halt();
241 return BT::NodeStatus::FAILURE;
242 }
243
244 // If client has been set up in derived constructor, event though this constructor couldn't, we discard the intention
245 // of dynamically creating the client
246 if (dynamic_client_instance_ && client_instance_) {
247 dynamic_client_instance_ = false;
248 }
249
250 // Try again to create the client on first tick if this was not possible during construction or if client should be
251 // created from a blackboard entry on the start of every iteration
252 if (status() == BT::NodeStatus::IDLE && dynamic_client_instance_) {
253 const BT::Expected<std::string> expected_name = context_.getTopicName(this);
254 if (expected_name) {
255 createClient(expected_name.value());
256 } else {
257 throw exceptions::RosNodeError(
258 context_.getFullyQualifiedTreeNodeName(this) +
259 " - Cannot create the service client because the service name couldn't be resolved using "
260 "the expression specified by the node's registration parameters (" +
261 NodeRegistrationOptions::PARAM_NAME_ROS2TOPIC + ": " + context_.getRegistrationOptions().topic +
262 "). Error message: " + expected_name.error());
263 }
264 }
265
266 if (!client_instance_) {
267 throw exceptions::RosNodeError(context_.getFullyQualifiedTreeNodeName(this) + " - client_instance_ is nullptr.");
268 }
269
270 auto & service_client = client_instance_->service_client;
271
272 auto check_status = [this](BT::NodeStatus status) {
273 if (!isStatusCompleted(status)) {
274 throw exceptions::RosNodeError(
275 context_.getFullyQualifiedTreeNodeName(this) + " - The callback must return either SUCCESS or FAILURE.");
276 }
277 return status;
278 };
279
280 // first step to be done only at the beginning of the Action
281 if (status() == BT::NodeStatus::IDLE) {
282 setStatus(BT::NodeStatus::RUNNING);
283
284 on_feedback_state_change_ = BT::NodeStatus::RUNNING;
285 response_ = {};
286
287 typename Request::SharedPtr request = std::make_shared<Request>();
288
289 if (!setRequest(request)) {
290 return check_status(onFailure(INVALID_REQUEST));
291 }
292
293 // Check if server is ready
294 if (!service_client->service_is_ready()) {
295 return onFailure(SERVICE_UNREACHABLE);
296 }
297
298 const auto future_and_request_id =
299 service_client->async_send_request(request, [this](typename ServiceClient::SharedFuture response) {
300 if (response.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
301 throw exceptions::RosNodeError(
302 this->context_.getFullyQualifiedTreeNodeName(this) + " - Response not ready in response callback.");
303 }
304 this->response_ = response.get();
305 });
306 future_ = future_and_request_id.future;
307 request_id_ = future_and_request_id.request_id;
308 time_request_sent_ = context_.getCurrentTime();
309
310 RCLCPP_DEBUG(logger_, "%s - Service request sent.", context_.getFullyQualifiedTreeNodeName(this).c_str());
311 return BT::NodeStatus::RUNNING;
312 }
313
314 if (status() == BT::NodeStatus::RUNNING) {
315 // FIRST case: check if the goal request has a timeout
316 if (!response_) {
317 // See if we must time out
318 if ((context_.getCurrentTime() - time_request_sent_) > context_.getRegistrationOptions().request_timeout) {
319 // Remove the pending request with the client if timed out
320 client_instance_->service_client->remove_pending_request(request_id_);
321 return check_status(onFailure(SERVICE_TIMEOUT));
322 }
323 return BT::NodeStatus::RUNNING;
324 } else if (future_.valid()) {
325 // Invalidate future since it's obsolete now and it indicates that we've done this step
326 future_ = {};
327
328 RCLCPP_DEBUG(logger_, "%s - Service response received.", context_.getFullyQualifiedTreeNodeName(this).c_str());
329 }
330
331 // SECOND case: response received
332 return check_status(onResponseReceived(response_));
333 }
334 return BT::NodeStatus::RUNNING;
335}
336
337template <class ServiceT>
338inline void RosServiceNode<ServiceT>::halt()
339{
340 if (status() == BT::NodeStatus::RUNNING) {
341 resetStatus();
342 }
343}
344
345template <class ServiceT>
346inline bool RosServiceNode<ServiceT>::setRequest(typename Request::SharedPtr & /*request*/)
347{
348 return true;
349}
350
351template <class ServiceT>
352inline BT::NodeStatus RosServiceNode<ServiceT>::onResponseReceived(const typename Response::SharedPtr & /*response*/)
353{
354 return BT::NodeStatus::SUCCESS;
355}
356
357template <class ServiceT>
358inline BT::NodeStatus RosServiceNode<ServiceT>::onFailure(ServiceNodeErrorCode error)
359{
360 const std::string msg = context_.getFullyQualifiedTreeNodeName(this) + " - Unexpected error " +
361 std::to_string(error) + ": " + toStr(error) + ".";
362 RCLCPP_ERROR_STREAM(logger_, msg);
363 throw exceptions::RosNodeError(msg);
364}
365
366template <class ServiceT>
367inline bool RosServiceNode<ServiceT>::createClient(const std::string & service_name)
368{
369 if (service_name.empty()) {
370 throw exceptions::RosNodeError(
371 context_.getFullyQualifiedTreeNodeName(this) +
372 " - Argument service_name is empty when trying to create the client.");
373 }
374
375 // Check if the service with given name is already set up
376 if (
377 client_instance_ && service_name == client_instance_->name &&
378 client_instance_->service_client->service_is_ready()) {
379 return true;
380 }
381
382 rclcpp::Node::SharedPtr node = context_.getRosNode();
383 rclcpp::CallbackGroup::SharedPtr group = context_.getWaitablesCallbackGroup();
384 if (!group) {
385 throw exceptions::RosNodeError(
386 context_.getFullyQualifiedTreeNodeName(this) +
387 " - The weak pointer to the ROS 2 callback group expired. The tree node doesn't "
388 "take ownership of it.");
389 }
390
391 // Reuse a service client shared across tree nodes with the same service name, or create one on first use.
392 client_instance_ = this->template getSharedEntity<ServiceClientInstance>(
393 service_name, [&] { return std::make_shared<ServiceClientInstance>(node, group, service_name); });
394
395 bool found = client_instance_->service_client->wait_for_service(context_.getRegistrationOptions().wait_timeout);
396 if (!found) {
397 std::string msg = context_.getFullyQualifiedTreeNodeName(this) + " - Service with name '" + client_instance_->name +
398 "' is not reachable.";
399 if (context_.getRegistrationOptions().allow_unreachable) {
400 RCLCPP_WARN_STREAM(logger_, msg);
401 } else {
402 RCLCPP_ERROR_STREAM(logger_, msg);
403 throw exceptions::RosNodeError(msg);
404 }
405 }
406 return found;
407}
408
409template <class ServiceT>
411{
412 if (client_instance_) return client_instance_->name;
413 return "unknown";
414}
415
416} // namespace auto_apms_behavior_tree::core
std::shared_ptr< InstanceT > getSharedEntity(const std::string &entity_name, const std::function< std::shared_ptr< InstanceT >()> &factory)
Retrieve a process-wide shared ROS 2 entity, creating it via factory on first use.
Additional parameters specific to ROS 2 determined at runtime by TreeBuilder.
std::string getServiceName() const
Get the name of the service this node connects with.
virtual bool setRequest(typename Request::SharedPtr &request)
Set the request to be sent to the ROS 2 service.
virtual BT::NodeStatus onResponseReceived(const typename Response::SharedPtr &response)
Callback invoked after the service response was received.
bool createClient(const std::string &service_name)
Create the client of the ROS 2 service.
static BT::PortsList providedBasicPorts(BT::PortsList addition)
ADerived nodes implementing the static method RosServiceNode::providedPorts may call this method to a...
virtual BT::NodeStatus onFailure(ServiceNodeErrorCode error)
Callback invoked when one of the errors in ServiceNodeErrorCode occur.
RosServiceNode(const std::string &instance_name, const Config &config, Context context)
Constructor.
static BT::PortsList providedPorts()
If a behavior tree requires input/output data ports, the developer must define this method accordingl...
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32
const char * toStr(const ActionNodeErrorCode &err)
Convert the action error code to string.