AutoAPMS
Resilient Robot Mission Management
Loading...
Searching...
No Matches
ros_service_node.hpp
Go to the documentation of this file.
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 <memory>
18#include <string>
19
20#include "rclcpp/executors.hpp"
21#include "behaviortree_cpp/action_node.h"
22
25
27{
28
36
37inline const char* toStr(const ServiceNodeErrorCode& err)
38{
39 switch (err)
40 {
42 return "SERVICE_UNREACHABLE";
43 case SERVICE_TIMEOUT:
44 return "SERVICE_TIMEOUT";
45 case INVALID_REQUEST:
46 return "INVALID_REQUEST";
47 case SERVICE_ABORTED:
48 return "SERVICE_ABORTED";
49 }
50 return nullptr;
51}
52
71template <class ServiceT>
72class RosServiceNode : public BT::ActionNodeBase
73{
74 using ServiceClient = typename rclcpp::Client<ServiceT>;
75 using ServiceClientPtr = std::shared_ptr<ServiceClient>;
76
77 struct ServiceClientInstance
78 {
79 ServiceClientInstance(std::shared_ptr<rclcpp::Node> node, const std::string& service_name);
80
81 ServiceClientPtr service_client;
82 rclcpp::CallbackGroup::SharedPtr callback_group;
83 rclcpp::executors::SingleThreadedExecutor callback_executor;
84 };
85
86 using ClientsRegistry = std::unordered_map<std::string, std::weak_ptr<ServiceClientInstance>>;
87
88public:
89 using ServiceType = ServiceT;
90 using Request = typename ServiceT::Request;
91 using Response = typename ServiceT::Response;
92 using Config = BT::NodeConfig;
94
95 explicit RosServiceNode(const std::string& instance_name, const Config& config, const Context& context);
96
97 virtual ~RosServiceNode() = default;
98
106 static BT::PortsList providedBasicPorts(BT::PortsList addition)
107 {
108 BT::PortsList basic = { BT::InputPort<std::string>("service_name", "", "Service name") };
109 basic.insert(addition.begin(), addition.end());
110 return basic;
111 }
112
117 static BT::PortsList providedPorts()
118 {
119 return providedBasicPorts({});
120 }
121
122 BT::NodeStatus tick() override;
123
125 void halt() override;
126
135 virtual bool setRequest(typename Request::SharedPtr& request);
136
140 virtual BT::NodeStatus onResponseReceived(const typename Response::SharedPtr& response);
141
145 virtual BT::NodeStatus onFailure(ServiceNodeErrorCode error);
146
147 std::string getFullName() const;
148
149protected:
150 const Context& getRosContext();
151
152 static std::mutex& getMutex();
153
154 // method to set the service name programmatically
155 void setServiceName(const std::string& service_name);
156
157 // contains the fully-qualified name of the node and the name of the client
158 static ClientsRegistry& getRegistry();
159
160 const rclcpp::Logger logger_;
161
162private:
163 const Context context_;
164 std::string service_name_;
165 bool service_name_should_be_checked_ = false;
166 std::shared_ptr<ServiceClientInstance> srv_instance_;
167 std::shared_future<typename Response::SharedPtr> future_response_;
168 rclcpp::Time time_request_sent_;
169 BT::NodeStatus on_feedback_state_change_;
170 bool response_received_;
171 typename Response::SharedPtr response_;
172
173 bool createClient(const std::string& service_name);
174};
175
176//----------------------------------------------------------------
177//---------------------- DEFINITIONS -----------------------------
178//----------------------------------------------------------------
179
180template <class ServiceT>
181inline RosServiceNode<ServiceT>::ServiceClientInstance::ServiceClientInstance(std::shared_ptr<rclcpp::Node> node,
182 const std::string& service_name)
183{
184 callback_group = node->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive, false);
185 callback_executor.add_callback_group(callback_group, node->get_node_base_interface());
186
187 service_client = node->create_client<ServiceT>(service_name, rmw_qos_profile_services_default, callback_group);
188}
189
190template <class ServiceT>
191inline RosServiceNode<ServiceT>::RosServiceNode(const std::string& instance_name, const Config& config,
192 const Context& context)
193 : BT::ActionNodeBase(instance_name, config), logger_(context.getLogger()), context_(context)
194{
195 // check port remapping
196 auto portIt = config.input_ports.find("service_name");
197 if (portIt != config.input_ports.end())
198 {
199 const std::string& bb_service_name = portIt->second;
200
201 if (isBlackboardPointer(bb_service_name))
202 {
203 // unknown value at construction time. postpone to tick
204 service_name_should_be_checked_ = true;
205 }
206 else if (!bb_service_name.empty())
207 {
208 // "hard-coded" name in the bb_service_name. Use it.
209 createClient(bb_service_name);
210 }
211 }
212 // no port value or it is empty. Use the default port value
213 if (!srv_instance_ && !context.default_port_name.empty())
214 {
215 createClient(context.default_port_name);
216 }
217}
218
219template <class ServiceT>
220inline bool RosServiceNode<ServiceT>::createClient(const std::string& service_name)
221{
222 if (service_name.empty())
223 {
224 throw exceptions::RosNodeError(getFullName() + " - Argument service_name is empty when trying to create a client.");
225 }
226
227 std::unique_lock lk(getMutex());
228 auto node = getRosContext().nh.lock();
229 if (!node)
230 {
231 throw exceptions::RosNodeError(getFullName() +
232 " - The shared pointer to the ROS node went out of scope. The tree node doesn't "
233 "take the ownership of the node.");
234 }
235 auto client_key = std::string(node->get_fully_qualified_name()) + "/" + service_name;
236
237 auto& registry = getRegistry();
238 auto it = registry.find(client_key);
239 if (it == registry.end() || it->second.expired())
240 {
241 srv_instance_ = std::make_shared<ServiceClientInstance>(node, service_name);
242 registry.insert({ client_key, srv_instance_ });
243 RCLCPP_DEBUG(logger_, "%s - Created client for service '%s'.", getFullName().c_str(), service_name.c_str());
244 }
245 else
246 {
247 srv_instance_ = it->second.lock();
248 }
249 service_name_ = service_name;
250
251 bool found = srv_instance_->service_client->wait_for_service(getRosContext().wait_for_server_timeout);
252 if (!found)
253 {
254 RCLCPP_WARN(logger_, "%s - Service with name '%s' is not reachable.", getFullName().c_str(), service_name_.c_str());
255 }
256 return found;
257}
258
259template <class ServiceT>
260inline BT::NodeStatus RosServiceNode<ServiceT>::tick()
261{
262 if (!rclcpp::ok())
263 {
264 halt();
265 return BT::NodeStatus::FAILURE;
266 }
267
268 // First, check if the service_client is valid and that the name of the
269 // service_name in the port didn't change.
270 // otherwise, create a new client
271 if (!srv_instance_ || (status() == BT::NodeStatus::IDLE && service_name_should_be_checked_))
272 {
273 std::string service_name;
274 getInput("service_name", service_name);
275 if (service_name_ != service_name)
276 {
277 createClient(service_name);
278 }
279 }
280
281 if (!srv_instance_)
282 {
283 throw exceptions::RosNodeError(getFullName() +
284 " - You must specify a service name either by using a default value or by "
285 "passing a value to the corresponding dynamic input port.");
286 }
287
288 auto check_status = [this](BT::NodeStatus status) {
289 if (!isStatusCompleted(status))
290 {
291 throw exceptions::RosNodeError(getFullName() + " - The callback must return either SUCCESS or FAILURE.");
292 }
293 return status;
294 };
295
296 // first step to be done only at the beginning of the Action
297 if (status() == BT::NodeStatus::IDLE)
298 {
299 setStatus(BT::NodeStatus::RUNNING);
300
301 response_received_ = false;
302 future_response_ = {};
303 on_feedback_state_change_ = BT::NodeStatus::RUNNING;
304 response_ = {};
305
306 typename Request::SharedPtr request = std::make_shared<Request>();
307
308 if (!setRequest(request))
309 {
310 return check_status(onFailure(INVALID_REQUEST));
311 }
312
313 // Check if server is ready
314 if (!srv_instance_->service_client->service_is_ready())
315 {
316 return onFailure(SERVICE_UNREACHABLE);
317 }
318
319 future_response_ = srv_instance_->service_client->async_send_request(request).share();
320 time_request_sent_ = getRosContext().getCurrentTime();
321
322 return BT::NodeStatus::RUNNING;
323 }
324
325 if (status() == BT::NodeStatus::RUNNING)
326 {
327 srv_instance_->callback_executor.spin_some();
328
329 // FIRST case: check if the goal request has a timeout
330 if (!response_received_)
331 {
332 auto ret = srv_instance_->callback_executor.spin_until_future_complete(future_response_, std::chrono::seconds(0));
333
334 if (ret != rclcpp::FutureReturnCode::SUCCESS)
335 {
336 if ((getRosContext().getCurrentTime() - time_request_sent_) > getRosContext().request_timeout)
337 {
338 return check_status(onFailure(SERVICE_TIMEOUT));
339 }
340 else
341 {
342 return BT::NodeStatus::RUNNING;
343 }
344 }
345 else
346 {
347 response_received_ = true;
348 response_ = future_response_.get();
349 future_response_ = {};
350
351 if (!response_)
352 {
353 throw exceptions::RosNodeError(getFullName() + " - Request was rejected by the service.");
354 }
355 }
356 }
357
358 // SECOND case: response received
359 return check_status(onResponseReceived(response_));
360 }
361 return BT::NodeStatus::RUNNING;
362}
363
364template <class ServiceT>
366{
367 if (status() == BT::NodeStatus::RUNNING)
368 {
369 resetStatus();
370 }
371}
372
373template <class ServiceT>
374inline bool RosServiceNode<ServiceT>::setRequest(typename Request::SharedPtr& /*request*/)
375{
376 return true;
377}
378
379template <class ServiceT>
380inline BT::NodeStatus RosServiceNode<ServiceT>::onResponseReceived(const typename Response::SharedPtr& /*response*/)
381{
382 return BT::NodeStatus::SUCCESS;
383}
384
385template <class ServiceT>
387{
388 RCLCPP_ERROR(logger_, "%s - Error %d: %s", getFullName().c_str(), error, toStr(error));
389 return BT::NodeStatus::FAILURE;
390}
391
392template <class ServiceT>
394{
395 // NOTE: registrationName() is empty during construction as this member is first set after the factory constructed the
396 // object
397 if (registrationName().empty())
398 return name();
399 if (this->name() == this->registrationName())
400 return this->name();
401 return this->name() + " (Type: " + this->registrationName() + ")";
402}
403
404template <class ServiceT>
406{
407 return context_;
408}
409
410template <class ServiceT>
412{
413 static std::mutex action_client_mutex;
414 return action_client_mutex;
415}
416
417template <class ServiceT>
418inline void RosServiceNode<ServiceT>::setServiceName(const std::string& service_name)
419{
420 service_name_ = service_name;
421 createClient(service_name);
422}
423
424template <class ServiceT>
425inline typename RosServiceNode<ServiceT>::ClientsRegistry& RosServiceNode<ServiceT>::getRegistry()
426{
427 static ClientsRegistry clients_registry;
428 return clients_registry;
429}
430
431} // namespace auto_apms_behavior_tree
Abstract class use to wrap rclcpp::Client<>
RosServiceNode(const std::string &instance_name, const Config &config, const Context &context)
virtual bool setRequest(typename Request::SharedPtr &request)
void setServiceName(const std::string &service_name)
void halt() override
The default halt() implementation.
virtual BT::NodeStatus onResponseReceived(const typename Response::SharedPtr &response)
static BT::PortsList providedBasicPorts(BT::PortsList addition)
Any subclass of RosServiceNode that has ports must implement a providedPorts method and call provided...
virtual BT::NodeStatus onFailure(ServiceNodeErrorCode error)
static BT::PortsList providedPorts()
Creates list of BT ports.
std::string toStr(TreeExecutor::ExecutionState state)
Definition executor.cpp:290