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