AutoAPMS
Resilient Robot Mission Management
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// https://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/ros_node_context.hpp"
23#include "auto_apms_util/string.hpp"
24#include "behaviortree_cpp/action_node.h"
25#include "rclcpp/executors.hpp"
26
28{
29
30enum ServiceNodeErrorCode
31{
32 SERVICE_UNREACHABLE,
33 SERVICE_TIMEOUT,
34 INVALID_REQUEST
35};
36
42inline const char * toStr(const ServiceNodeErrorCode & err)
43{
44 switch (err) {
45 case SERVICE_UNREACHABLE:
46 return "SERVICE_UNREACHABLE";
47 case SERVICE_TIMEOUT:
48 return "SERVICE_TIMEOUT";
49 case INVALID_REQUEST:
50 return "INVALID_REQUEST";
51 }
52 return nullptr;
53}
54
90template <class ServiceT>
91class RosServiceNode : public BT::ActionNodeBase
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
105 using ClientsRegistry = std::unordered_map<std::string, std::weak_ptr<ServiceClientInstance>>;
106
107public:
108 using ServiceType = ServiceT;
109 using Request = typename ServiceT::Request;
110 using Response = typename ServiceT::Response;
111 using Config = BT::NodeConfig;
112 using Context = RosNodeContext;
113
123 explicit RosServiceNode(const std::string & instance_name, const Config & config, Context context);
124
125 virtual ~RosServiceNode() = default;
126
134 static BT::PortsList providedBasicPorts(BT::PortsList addition)
135 {
136 BT::PortsList basic = {BT::InputPort<std::string>("port", "Name of the ROS 2 service.")};
137 basic.insert(addition.begin(), addition.end());
138 return basic;
139 }
140
145 static BT::PortsList providedPorts() { return providedBasicPorts({}); }
146
156 virtual bool setRequest(typename Request::SharedPtr & request);
157
167 virtual BT::NodeStatus onResponseReceived(const typename Response::SharedPtr & response);
168
178 virtual BT::NodeStatus onFailure(ServiceNodeErrorCode error);
179
185 bool createClient(const std::string & service_name);
186
191 std::string getServiceName() const;
192
193protected:
194 const Context context_;
195 const rclcpp::Logger logger_;
196
197private:
198 BT::NodeStatus tick() override;
199
200 void halt() override;
201
202 static std::mutex & getMutex();
203
204 static ClientsRegistry & getRegistry();
205
206private:
207 bool dynamic_client_instance_ = false;
208 std::shared_ptr<ServiceClientInstance> client_instance_;
209 typename ServiceClient::SharedFuture future_;
210 int64_t request_id_;
211 rclcpp::Time time_request_sent_;
212 BT::NodeStatus on_feedback_state_change_;
213 typename Response::SharedPtr response_;
214};
215
216// #####################################################################################################################
217// ################################ DEFINITIONS ##############################################
218// #####################################################################################################################
219
220template <class ServiceT>
221inline RosServiceNode<ServiceT>::ServiceClientInstance::ServiceClientInstance(
222 rclcpp::Node::SharedPtr node, rclcpp::CallbackGroup::SharedPtr group, const std::string & service_name)
223{
224 service_client = node->create_client<ServiceT>(service_name, rmw_qos_profile_services_default, group);
225 name = service_name;
226}
227
228template <class ServiceT>
230 const std::string & instance_name, const Config & config, Context context)
231: BT::ActionNodeBase(instance_name, config),
232 context_(context),
233 logger_(context.getChildLogger(auto_apms_util::toSnakeCase(instance_name)))
234{
235 if (const BT::Expected<std::string> expected_name = context_.getCommunicationPortName(this)) {
236 createClient(expected_name.value());
237 } else {
238 // We assume that determining the communication port requires a blackboard pointer, which cannot be evaluated at
239 // construction time. The expression will be evaluated each time before the node is ticked the first time after
240 // successful execution.
241 dynamic_client_instance_ = true;
242 }
243}
244
245template <class ServiceT>
246inline BT::NodeStatus RosServiceNode<ServiceT>::tick()
247{
248 if (!rclcpp::ok()) {
249 halt();
250 return BT::NodeStatus::FAILURE;
251 }
252
253 // If client has been set up in derived constructor, event though this constructor couldn't, we discard the intention
254 // of dynamically creating the client
255 if (dynamic_client_instance_ && client_instance_) {
256 dynamic_client_instance_ = false;
257 }
258
259 // Try again to create the client on first tick if this was not possible during construction or if client should be
260 // created from a blackboard entry on the start of every iteration
261 if (status() == BT::NodeStatus::IDLE && dynamic_client_instance_) {
262 const BT::Expected<std::string> expected_name = context_.getCommunicationPortName(this);
263 if (expected_name) {
264 createClient(expected_name.value());
265 } else {
266 throw exceptions::RosNodeError(
267 context_.getFullyQualifiedTreeNodeName(this) +
268 " - Cannot create the service client because the service name couldn't be resolved using "
269 "the communication port expression specified by the node's "
270 "registration parameters (" +
271 NodeRegistrationOptions::PARAM_NAME_PORT + ": " + context_.registration_options_.port +
272 "). Error message: " + expected_name.error());
273 }
274 }
275
276 if (!client_instance_) {
277 throw exceptions::RosNodeError(context_.getFullyQualifiedTreeNodeName(this) + " - client_instance_ is nullptr.");
278 }
279
280 auto & service_client = client_instance_->service_client;
281
282 auto check_status = [this](BT::NodeStatus status) {
283 if (!isStatusCompleted(status)) {
284 throw exceptions::RosNodeError(
285 context_.getFullyQualifiedTreeNodeName(this) + " - The callback must return either SUCCESS or FAILURE.");
286 }
287 return status;
288 };
289
290 // first step to be done only at the beginning of the Action
291 if (status() == BT::NodeStatus::IDLE) {
292 setStatus(BT::NodeStatus::RUNNING);
293
294 on_feedback_state_change_ = BT::NodeStatus::RUNNING;
295 response_ = {};
296
297 typename Request::SharedPtr request = std::make_shared<Request>();
298
299 if (!setRequest(request)) {
300 return check_status(onFailure(INVALID_REQUEST));
301 }
302
303 // Check if server is ready
304 if (!service_client->service_is_ready()) {
305 return onFailure(SERVICE_UNREACHABLE);
306 }
307
308 const auto future_and_request_id =
309 service_client->async_send_request(request, [this](typename ServiceClient::SharedFuture response) {
310 if (response.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
311 throw exceptions::RosNodeError(
312 this->context_.getFullyQualifiedTreeNodeName(this) + " - Response not ready in response callback.");
313 }
314 this->response_ = response.get();
315 });
316 future_ = future_and_request_id.future;
317 request_id_ = future_and_request_id.request_id;
318 time_request_sent_ = context_.getCurrentTime();
319
320 RCLCPP_DEBUG(logger_, "%s - Service request sent.", context_.getFullyQualifiedTreeNodeName(this).c_str());
321 return BT::NodeStatus::RUNNING;
322 }
323
324 if (status() == BT::NodeStatus::RUNNING) {
325 // FIRST case: check if the goal request has a timeout
326 if (!response_) {
327 // See if we must time out
328 if ((context_.getCurrentTime() - time_request_sent_) > context_.registration_options_.request_timeout) {
329 // Remove the pending request with the client if timed out
330 client_instance_->service_client->remove_pending_request(request_id_);
331 return check_status(onFailure(SERVICE_TIMEOUT));
332 }
333 return BT::NodeStatus::RUNNING;
334 } else if (future_.valid()) {
335 // Invalidate future since it's obsolete now and it indicates that we've done this step
336 future_ = {};
337
338 RCLCPP_DEBUG(logger_, "%s - Service response received.", context_.getFullyQualifiedTreeNodeName(this).c_str());
339 }
340
341 // SECOND case: response received
342 return check_status(onResponseReceived(response_));
343 }
344 return BT::NodeStatus::RUNNING;
345}
346
347template <class ServiceT>
348inline void RosServiceNode<ServiceT>::halt()
349{
350 if (status() == BT::NodeStatus::RUNNING) {
351 resetStatus();
352 }
353}
354
355template <class ServiceT>
356inline bool RosServiceNode<ServiceT>::setRequest(typename Request::SharedPtr & /*request*/)
357{
358 return true;
359}
360
361template <class ServiceT>
362inline BT::NodeStatus RosServiceNode<ServiceT>::onResponseReceived(const typename Response::SharedPtr & /*response*/)
363{
364 return BT::NodeStatus::SUCCESS;
365}
366
367template <class ServiceT>
368inline BT::NodeStatus RosServiceNode<ServiceT>::onFailure(ServiceNodeErrorCode error)
369{
370 const std::string msg = context_.getFullyQualifiedTreeNodeName(this) + " - Unexpected error " +
371 std::to_string(error) + ": " + toStr(error) + ".";
372 RCLCPP_ERROR_STREAM(logger_, msg);
373 throw exceptions::RosNodeError(msg);
374}
375
376template <class ServiceT>
377inline bool RosServiceNode<ServiceT>::createClient(const std::string & service_name)
378{
379 if (service_name.empty()) {
380 throw exceptions::RosNodeError(
381 context_.getFullyQualifiedTreeNodeName(this) +
382 " - Argument service_name is empty when trying to create the client.");
383 }
384
385 // Check if the service with given name is already set up
386 if (
387 client_instance_ && service_name == client_instance_->name &&
388 client_instance_->service_client->service_is_ready()) {
389 return true;
390 }
391
392 std::unique_lock lk(getMutex());
393
394 rclcpp::Node::SharedPtr node = context_.nh_.lock();
395 if (!node) {
396 throw exceptions::RosNodeError(
397 context_.getFullyQualifiedTreeNodeName(this) +
398 " - The weak pointer to the ROS 2 node expired. The tree node doesn't "
399 "take ownership of it.");
400 }
401 rclcpp::CallbackGroup::SharedPtr group = context_.cb_group_.lock();
402 if (!group) {
403 throw exceptions::RosNodeError(
404 context_.getFullyQualifiedTreeNodeName(this) +
405 " - The weak pointer to the ROS 2 callback group expired. The tree node doesn't "
406 "take ownership of it.");
407 }
408 auto client_key = std::string(node->get_fully_qualified_name()) + "/" + service_name;
409
410 auto & registry = getRegistry();
411 auto it = registry.find(client_key);
412 if (it == registry.end() || it->second.expired()) {
413 client_instance_ = std::make_shared<ServiceClientInstance>(node, group, service_name);
414 registry.insert({client_key, client_instance_});
415 RCLCPP_DEBUG(
416 logger_, "%s - Created client for service '%s'.", context_.getFullyQualifiedTreeNodeName(this).c_str(),
417 service_name.c_str());
418 } else {
419 client_instance_ = it->second.lock();
420 }
421
422 bool found = client_instance_->service_client->wait_for_service(context_.registration_options_.wait_timeout);
423 if (!found) {
424 std::string msg = context_.getFullyQualifiedTreeNodeName(this) + " - Service with name '" + client_instance_->name +
425 "' is not reachable.";
426 if (context_.registration_options_.allow_unreachable) {
427 RCLCPP_WARN_STREAM(logger_, msg);
428 } else {
429 RCLCPP_ERROR_STREAM(logger_, msg);
430 throw exceptions::RosNodeError(msg);
431 }
432 }
433 return found;
434}
435
436template <class ServiceT>
438{
439 if (client_instance_) return client_instance_->name;
440 return "unkown";
441}
442
443template <class ServiceT>
444inline std::mutex & RosServiceNode<ServiceT>::getMutex()
445{
446 static std::mutex action_client_mutex;
447 return action_client_mutex;
448}
449
450template <class ServiceT>
451inline typename RosServiceNode<ServiceT>::ClientsRegistry & RosServiceNode<ServiceT>::getRegistry()
452{
453 static ClientsRegistry clients_registry;
454 return clients_registry;
455}
456
457} // namespace auto_apms_behavior_tree::core
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 builder.hpp:30
const char * toStr(const ActionNodeErrorCode &err)
Convert the action error code to string.
Fundamental helper classes and utility functions.