AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
mode_executor.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 <optional>
18
19#include "auto_apms_px4/mode.hpp"
20#include "auto_apms_px4/vehicle_command_client.hpp"
21#include "auto_apms_util/action_wrapper.hpp"
22#include "px4_msgs/msg/mode_completed.hpp"
23#include "px4_msgs/msg/vehicle_status.hpp"
24#include "px4_ros2/components/wait_for_fmu.hpp"
25#include "px4_ros2/utils/message_version.hpp"
26
40
46namespace auto_apms_px4
47{
48
95template <class ActionT>
96class ModeExecutor : public auto_apms_util::ActionWrapper<ActionT>
97{
98 enum class State : uint8_t
99 {
100 REQUEST_ACTIVATION,
101 WAIT_FOR_ACTIVATION,
102 WAIT_FOR_COMPLETION_SIGNAL,
103 COMPLETE
104 };
105
106public:
107 using VehicleCommandClient = auto_apms_px4::VehicleCommandClient;
108 using FlightMode = VehicleCommandClient::FlightMode;
109 using typename auto_apms_util::ActionWrapper<ActionT>::ActionContextType;
110 using typename auto_apms_util::ActionWrapper<ActionT>::Goal;
111 using typename auto_apms_util::ActionWrapper<ActionT>::Feedback;
112 using typename auto_apms_util::ActionWrapper<ActionT>::Result;
113 using ActionStatus = auto_apms_util::ActionStatus;
114
115 explicit ModeExecutor(
116 const std::string & action_name, rclcpp::Node::SharedPtr node_ptr,
117 std::shared_ptr<ActionContextType> action_context_ptr, uint8_t mode_id,
118 FlightMode deactivation_flight_mode = FlightMode::Hold, bool disarm_after_completion = false);
119 explicit ModeExecutor(
120 const std::string & action_name, const rclcpp::NodeOptions & options, uint8_t mode_id,
121 FlightMode deactivation_flight_mode = FlightMode::Hold, bool disarm_after_completion = false);
122 explicit ModeExecutor(
123 const std::string & action_name, const rclcpp::NodeOptions & options, FlightMode flight_mode,
124 FlightMode deactivation_flight_mode = FlightMode::Hold, bool disarm_after_completion = false);
125
126 static bool isExternalMode(uint8_t mode_id);
127
128private:
129 void setUp();
130 auto_apms_util::ActionStatus asyncDeactivateFlightMode();
131 bool onGoalRequest(std::shared_ptr<const Goal> goal_ptr) override final;
132 bool onCancelRequest(std::shared_ptr<const Goal> goal_ptr, std::shared_ptr<Result> result_ptr) override final;
134 std::shared_ptr<const Goal> goal_ptr, std::shared_ptr<Result> result_ptr) override final;
136 std::shared_ptr<const Goal> goal_ptr, std::shared_ptr<Feedback> feedback_ptr,
137 std::shared_ptr<Result> result_ptr) override final;
138
139protected:
140 bool isCurrentNavState(uint8_t nav_state);
141 virtual bool sendActivationCommand(const VehicleCommandClient & client, std::shared_ptr<const Goal> goal_ptr);
142 virtual bool isCompleted(std::shared_ptr<const Goal> goal_ptr, const px4_msgs::msg::VehicleStatus & vehicle_status);
143 virtual void setFeedback(std::shared_ptr<Feedback> feedback_ptr, const px4_msgs::msg::VehicleStatus & vehicle_status);
144
145 uint8_t getModeID() const;
146
147private:
148 const VehicleCommandClient vehicle_command_client_;
149 const uint8_t mode_id_;
150 bool is_custom_mode_{false}; // Whether the mode is a custom mode (i.e. not one of the standard PX4 modes)
151 FlightMode deactivation_flight_mode_;
152 bool disarm_after_completion_;
153 rclcpp::Subscription<px4_msgs::msg::VehicleStatus>::SharedPtr vehicle_status_sub_ptr_;
154 rclcpp::Subscription<px4_msgs::msg::ModeCompleted>::SharedPtr mode_completed_sub_ptr_;
155 px4_msgs::msg::VehicleStatus::SharedPtr last_vehicle_status_ptr_;
156 std::optional<uint8_t> mode_completed_result_;
157 bool deactivation_command_sent_{false};
158 State state_{State::REQUEST_ACTIVATION};
159 rclcpp::Time activation_command_sent_time_;
160 rclcpp::Duration activation_timeout_{0, 0};
161};
162
170template <class ActionT, class ModeT>
171class ModeExecutorFactory
172{
173public:
174 ModeExecutorFactory(
175 const std::string & action_name, const rclcpp::NodeOptions & options,
176 VehicleCommandClient::FlightMode deactivation_flight_mode = VehicleCommandClient::FlightMode::Hold,
177 bool disarm_after_completion = false);
178
179 rclcpp::node_interfaces::NodeBaseInterface::SharedPtr get_node_base_interface();
180
181private:
182 rclcpp::Node::SharedPtr node_ptr_; // It's necessary to also store the node pointer here for successful destruction
183 std::unique_ptr<ModeBase<ActionT>> mode_ptr_;
184 std::shared_ptr<ModeExecutor<ActionT>> mode_executor_ptr_;
185};
186
187// #####################################################################################################################
188// ################################ DEFINITIONS ##############################################
189// #####################################################################################################################
190
191template <class ActionT>
192ModeExecutor<ActionT>::ModeExecutor(
193 const std::string & action_name, rclcpp::Node::SharedPtr node_ptr,
194 std::shared_ptr<ActionContextType> action_context_ptr, uint8_t mode_id, FlightMode deactivation_flight_mode,
195 bool disarm_after_completion)
196: auto_apms_util::ActionWrapper<ActionT>(action_name, node_ptr, action_context_ptr),
197 vehicle_command_client_(*node_ptr),
198 mode_id_(mode_id),
199 deactivation_flight_mode_(deactivation_flight_mode),
200 disarm_after_completion_(disarm_after_completion)
201{
202 setUp();
203}
204
205template <class ActionT>
206ModeExecutor<ActionT>::ModeExecutor(
207 const std::string & action_name, const rclcpp::NodeOptions & options, uint8_t mode_id,
208 FlightMode deactivation_flight_mode, bool disarm_after_completion)
209: auto_apms_util::ActionWrapper<ActionT>(action_name, options),
210 vehicle_command_client_(*this->node_ptr_),
211 mode_id_(mode_id),
212 is_custom_mode_(isExternalMode(mode_id_)),
213 deactivation_flight_mode_(deactivation_flight_mode),
214 disarm_after_completion_(disarm_after_completion)
215{
216 setUp();
217}
218
219template <class ActionT>
220ModeExecutor<ActionT>::ModeExecutor(
221 const std::string & action_name, const rclcpp::NodeOptions & options, FlightMode flight_mode,
222 FlightMode deactivation_flight_mode, bool disarm_after_completion)
223: ModeExecutor<ActionT>(
224 action_name, options, static_cast<uint8_t>(flight_mode), deactivation_flight_mode, disarm_after_completion)
225{
226}
227
228template <class ActionT>
229inline bool ModeExecutor<ActionT>::isExternalMode(uint8_t mode_id)
230{
231 const uint8_t first_external_mode_nav_state = px4_msgs::msg::VehicleStatus::NAVIGATION_STATE_EXTERNAL1;
232 const uint8_t max_external_mode_nav_state = px4_msgs::msg::VehicleStatus::NAVIGATION_STATE_EXTERNAL8 + 1;
233 return mode_id >= first_external_mode_nav_state && mode_id < max_external_mode_nav_state;
234}
235
236template <class ActionT>
237void ModeExecutor<ActionT>::setUp()
238{
239 vehicle_status_sub_ptr_ = this->node_ptr_->template create_subscription<px4_msgs::msg::VehicleStatus>(
240 "fmu/out/vehicle_status" + px4_ros2::getMessageNameVersion<px4_msgs::msg::VehicleStatus>(),
241 rclcpp::QoS(1).best_effort(),
242 [this](px4_msgs::msg::VehicleStatus::UniquePtr msg) { last_vehicle_status_ptr_ = std::move(msg); });
243
244 mode_completed_sub_ptr_ = this->node_ptr_->template create_subscription<px4_msgs::msg::ModeCompleted>(
245 "fmu/out/mode_completed" + px4_ros2::getMessageNameVersion<px4_msgs::msg::ModeCompleted>(),
246 rclcpp::QoS(1).best_effort(), [this](px4_msgs::msg::ModeCompleted::UniquePtr msg) {
247 bool is_relevant = msg->nav_state == this->mode_id_;
248 RCLCPP_DEBUG(
249 this->node_ptr_->get_logger(),
250 "Flight mode completion signal received by mode %i. Signal was: timestamp=%li, mode_id=%i, result=%i (is "
251 "relevant for this mode: %i)",
252 this->mode_id_, msg->timestamp, msg->nav_state, msg->result, is_relevant);
253 if (is_relevant) {
254 this->mode_completed_result_ = msg->result;
255 }
256 });
257}
258
259template <class ActionT>
260auto_apms_util::ActionStatus ModeExecutor<ActionT>::asyncDeactivateFlightMode()
261{
262 // If currently waiting for flight mode activation and the deactivation mode is already active, we need to wait for
263 // the nav state to change before starting deactivation. Otherwise, we'll misinterpret the current nav state and
264 // return success immediately.
265 bool is_deactivation_mode_active = isCurrentNavState(static_cast<uint8_t>(deactivation_flight_mode_));
266 if (state_ == State::WAIT_FOR_ACTIVATION) {
267 if (is_deactivation_mode_active) {
268 auto & clock = *this->node_ptr_->get_clock();
269 RCLCPP_DEBUG_THROTTLE(
270 this->node_ptr_->get_logger(), clock, 250, "Waiting for flight mode %i to become active before deactivating...",
271 mode_id_);
272 return ActionStatus::RUNNING;
273 } else {
274 state_ = State::COMPLETE; // Change state to indicate that mode has been activated
275 }
276 }
277
278 if (is_deactivation_mode_active) {
279 RCLCPP_DEBUG(
280 this->node_ptr_->get_logger(), "Deactivated flight mode successfully (deactivation mode %i is active)",
281 static_cast<int>(deactivation_flight_mode_));
282 return ActionStatus::SUCCESS;
283 } else {
284 // Only send command if not in deactivation mode already
285 if (!deactivation_command_sent_) {
286 if (!vehicle_command_client_.syncActivateFlightMode(deactivation_flight_mode_)) {
287 RCLCPP_ERROR(
288 this->node_ptr_->get_logger(), "Failed to send command to activate deactivation flight mode %i",
289 static_cast<int>(deactivation_flight_mode_));
290 return ActionStatus::FAILURE;
291 }
292 // Force to consider only new status messages after sending new command
293 last_vehicle_status_ptr_ = nullptr;
294 deactivation_command_sent_ = true;
295 }
296 }
297
298 return ActionStatus::RUNNING;
299}
300
301template <class ActionT>
302bool ModeExecutor<ActionT>::onGoalRequest(const std::shared_ptr<const Goal> /*goal_ptr*/)
303{
304 state_ = State::REQUEST_ACTIVATION;
305 deactivation_command_sent_ = false;
306 mode_completed_result_ = std::nullopt;
307 activation_timeout_ = rclcpp::Duration::from_seconds(fmin(this->param_listener_.get_params().loop_rate * 15, 1.5));
308 return true;
309}
310
311template <class ActionT>
312bool ModeExecutor<ActionT>::onCancelRequest(
313 std::shared_ptr<const Goal> /*goal_ptr*/, std::shared_ptr<Result> /*result_ptr*/)
314{
315 RCLCPP_INFO(this->node_ptr_->get_logger(), "Cancellation requested!");
316 return true;
317}
318
319template <class ActionT>
320auto_apms_util::ActionStatus ModeExecutor<ActionT>::cancelGoal(
321 std::shared_ptr<const Goal> /*goal_ptr*/, std::shared_ptr<Result> /*result_ptr*/)
322{
323 // The custom mode is responsible for managing the lifecycle of the cancellation via the onGoalCanceled callback.
324 // Wait until the mode signals completion (by calling completed()) or PX4 deactivates it externally. For standard
325 // modes, continue immediately with deactivation since we can't rely on the mode_completed topic for standard modes as
326 // they usually don't publish to it.
327 if (is_custom_mode_ && state_ != State::COMPLETE) {
328 // During cancellation, we bypass the custom isCompleted method and simply wait for any mode completion signal
329 // regardless the value
330 if (mode_completed_result_.has_value()) {
331 state_ = State::COMPLETE;
332 } else {
333 if (!isCurrentNavState(mode_id_)) {
334 RCLCPP_WARN(
335 this->node_ptr_->get_logger(), "Flight mode %i was deactivated externally during cancellation", mode_id_);
336 // In this case we don't have to do anything afterwards
337 return ActionStatus::SUCCESS;
338 }
339 return ActionStatus::RUNNING;
340 }
341 }
342
343 ActionStatus ret = ActionStatus::SUCCESS;
344 if (deactivation_flight_mode_ != FlightMode::Unset) {
345 ret = asyncDeactivateFlightMode();
346 }
347
348 if (ret != ActionStatus::RUNNING) {
349 if (disarm_after_completion_ && !vehicle_command_client_.disarm()) {
350 RCLCPP_WARN(this->node_ptr_->get_logger(), "Failed to disarm after flight mode %i cancellation", mode_id_);
351 }
352 RCLCPP_INFO(this->node_ptr_->get_logger(), "Flight mode %i cancellation complete", mode_id_);
353 }
354 return ret;
355}
356
357template <class ActionT>
358bool ModeExecutor<ActionT>::isCurrentNavState(uint8_t nav_state)
359{
360 if (last_vehicle_status_ptr_ && last_vehicle_status_ptr_->nav_state == nav_state) {
361 return true;
362 }
363 return false;
364}
365
366template <class ActionT>
367auto_apms_util::ActionStatus ModeExecutor<ActionT>::executeGoal(
368 std::shared_ptr<const Goal> goal_ptr, std::shared_ptr<Feedback> feedback_ptr, std::shared_ptr<Result> /*result_ptr*/)
369{
370 switch (state_) {
371 case State::REQUEST_ACTIVATION:
372 if (!sendActivationCommand(vehicle_command_client_, goal_ptr)) {
373 RCLCPP_ERROR(
374 this->node_ptr_->get_logger(), "Failed to send activation command for flight mode %i. Aborting...", mode_id_);
375 return ActionStatus::FAILURE;
376 }
377 // Force to consider only new status messages after sending new command
378 last_vehicle_status_ptr_ = nullptr;
379 state_ = State::WAIT_FOR_ACTIVATION;
380 activation_command_sent_time_ = this->node_ptr_->now();
381 RCLCPP_DEBUG(
382 this->node_ptr_->get_logger(), "Activation command for flight mode %i was sent successfully", mode_id_);
383 return ActionStatus::RUNNING;
384 case State::WAIT_FOR_ACTIVATION:
385 if (isCurrentNavState(mode_id_)) {
386 RCLCPP_DEBUG(this->node_ptr_->get_logger(), "Flight mode %i is active", mode_id_);
387 state_ = State::WAIT_FOR_COMPLETION_SIGNAL;
388 } else if (this->node_ptr_->now() - activation_command_sent_time_ > activation_timeout_) {
389 RCLCPP_ERROR(this->node_ptr_->get_logger(), "Timeout activating flight mode %i. Aborting...", mode_id_);
390 return ActionStatus::FAILURE;
391 }
392 return ActionStatus::RUNNING;
393 case State::WAIT_FOR_COMPLETION_SIGNAL:
394 // Populate feedback message
395 setFeedback(feedback_ptr, *last_vehicle_status_ptr_);
396
397 // Check if execution should be terminated
398 if (isCompleted(goal_ptr, *last_vehicle_status_ptr_)) {
399 state_ = State::COMPLETE;
400 if (deactivation_flight_mode_ != FlightMode::Unset) {
401 RCLCPP_DEBUG(
402 this->node_ptr_->get_logger(),
403 "Flight mode %i complete! Will deactivate before termination (switching to flight mode %i)...", mode_id_,
404 static_cast<int>(deactivation_flight_mode_));
405 } else {
406 RCLCPP_DEBUG(
407 this->node_ptr_->get_logger(),
408 "Flight mode %i complete! Will leave current navigation state as is. User is "
409 "responsible for initiating the next flight mode...",
410 mode_id_);
411 }
412
413 // Don't return to complete in same iteration
414 break;
415 }
416 // Check if nav state changed
417 if (!isCurrentNavState(mode_id_)) {
418 RCLCPP_WARN(this->node_ptr_->get_logger(), "Flight mode %i was deactivated externally. Aborting...", mode_id_);
419 return ActionStatus::FAILURE;
420 }
421 return ActionStatus::RUNNING;
422 case State::COMPLETE:
423 break;
424 }
425
426 if (deactivation_flight_mode_ != FlightMode::Unset) {
427 const auto deactivation_state = asyncDeactivateFlightMode();
428 if (deactivation_state != ActionStatus::SUCCESS) {
429 return deactivation_state;
430 }
431 // Don't return to complete in same iteration
432 }
433
434 RCLCPP_INFO(this->node_ptr_->get_logger(), "Flight mode %i execution complete", mode_id_);
435 if (disarm_after_completion_ && !vehicle_command_client_.disarm()) {
436 RCLCPP_WARN(this->node_ptr_->get_logger(), "Failed to disarm after flight mode %i completion", mode_id_);
437 }
438 return ActionStatus::SUCCESS;
439}
440
441template <class ActionT>
442bool ModeExecutor<ActionT>::sendActivationCommand(
443 const VehicleCommandClient & client, std::shared_ptr<const Goal> /*goal_ptr*/)
444{
445 return client.syncActivateFlightMode(mode_id_);
446}
447
448template <class ActionT>
449bool ModeExecutor<ActionT>::isCompleted(
450 std::shared_ptr<const Goal> /*goal_ptr*/, const px4_msgs::msg::VehicleStatus & /*vehicle_status*/)
451{
452 if (!mode_completed_result_.has_value()) {
453 return false;
454 }
455 if (*mode_completed_result_ == px4_msgs::msg::ModeCompleted::RESULT_SUCCESS) {
456 return true;
457 } else {
458 RCLCPP_INFO(
459 this->node_ptr_->get_logger(), "Flight mode %i completed unsuccessfully (result: %i). Aborting...", mode_id_,
460 static_cast<int>(*mode_completed_result_));
461 this->action_context_ptr_->abort();
462 }
463 return false;
464}
465
466template <class ActionT>
467void ModeExecutor<ActionT>::setFeedback(
468 std::shared_ptr<Feedback> /*feedback_ptr*/, const px4_msgs::msg::VehicleStatus & /*vehicle_status*/)
469{
470 return;
471}
472
473template <class ActionT>
474uint8_t ModeExecutor<ActionT>::getModeID() const
475{
476 return mode_id_;
477}
478
479template <class ActionT, class ModeT>
480ModeExecutorFactory<ActionT, ModeT>::ModeExecutorFactory(
481 const std::string & action_name, const rclcpp::NodeOptions & options,
482 VehicleCommandClient::FlightMode deactivation_flight_mode, bool disarm_after_completion)
483: node_ptr_(std::make_shared<rclcpp::Node>(action_name + "_node", options))
484{
485 static_assert(
486 std::is_base_of<ModeBase<ActionT>, ModeT>::value,
487 "Template argument ModeT must inherit auto_apms::ModeBase<ActionT> as public and with same type "
488 "ActionT as auto_apms::ActionWrapper<ActionT>");
489
490 const auto action_context_ptr = std::make_shared<auto_apms_util::ActionContext<ActionT>>(node_ptr_->get_logger());
491
492 mode_ptr_ = std::make_unique<ModeT>(*node_ptr_, px4_ros2::ModeBase::Settings(action_name), action_context_ptr);
493
494 constexpr int max_retries = 5;
495 bool fmu_available = false;
496 for (int attempt = 0; attempt < max_retries; ++attempt) {
497 if (px4_ros2::waitForFMU(*node_ptr_, std::chrono::seconds(3))) {
498 fmu_available = true;
499 RCLCPP_DEBUG(node_ptr_->get_logger(), "FMU availability test successful (attempt %d).", attempt + 1);
500 break;
501 }
502 RCLCPP_WARN(node_ptr_->get_logger(), "No message from FMU (attempt %d/%d). Retrying...", attempt + 1, max_retries);
503 }
504 if (!fmu_available) {
505 throw std::runtime_error("No message from FMU after multiple attempts");
506 }
507
508 if (!mode_ptr_->doRegister()) {
509 RCLCPP_FATAL(node_ptr_->get_logger(), "Registration of mode with action_name: '%s' failed.", action_name.c_str());
510 throw std::runtime_error("Mode registration failed");
511 }
512
513 // AFTER (!) registration, the mode id can be queried to set up the executor
514 mode_executor_ptr_ = std::make_shared<ModeExecutor<ActionT>>(
515 action_name, node_ptr_, action_context_ptr, mode_ptr_->id(), deactivation_flight_mode, disarm_after_completion);
516}
517
518template <class ActionT, class ModeT>
519rclcpp::node_interfaces::NodeBaseInterface::SharedPtr ModeExecutorFactory<ActionT, ModeT>::get_node_base_interface()
520{
521 return node_ptr_->get_node_base_interface();
522}
523
524} // namespace auto_apms_px4
Generic template class for executing a PX4 mode implementing the interface of a standard ROS 2 action...
Client for sending requests to the PX4 autopilot using the VehicleCommand topic.
Generic base class for implementing robot skills using the ROS 2 action concept.
ActionStatus
Status of the auto_apms_util::ActionWrapper execution process.
Implementation of PX4 mode peers offered by px4_ros2_cpp enabling integration with AutoAPMS.
Definition mode.hpp:28
Fundamental helper classes and utility functions.