Modes

An operation mode is a state in which the autopilot can be in. The autopilot can be in only one mode at a time. In this section we provide an overview on the autopilot operating modes and how to add custom modes to the state machine.

Depending on the target application, an autopilot mode can be used to define a new behaviour for the vehicle to execute, or be used to prototype a controller as well. For instance, if you want to prototype a new controller to be used only for following a specific trajectory, then it might make sense to implement it as a mode rather than a controller that is used by all the other available modes.

This page is structured as follows:

0. Mode Interface

The interface for the autopilot operating modes is defined in the autopilot package under the pegasus_autopilot/autopilot/include/autopilot/mode.hpp header file. The Mode class is an abstract class that defines the interface for the autopilot operating modes.

The autopilot operating modes must inherit from the Mode class and implement the methods highlighted in yellow (lines 42-47) in the code snippet below:

 1namespace autopilot {
 2
 3class Mode {
 4
 5public:
 6
 7    using SharedPtr = std::shared_ptr<Mode>;
 8    using UniquePtr = std::unique_ptr<Mode>;
 9    using WeakPtr = std::weak_ptr<Mode>;
10
11    // Configuration for the operation mode
12    struct Config {
13        rclcpp::Node::SharedPtr node;                                           // ROS 2 node ptr (in case the mode needs to create publishers, subscribers, etc.)
14        std::function<State()> get_vehicle_state;                               // Function pointer to get the current state of the vehicle      
15        std::function<VehicleStatus()> get_vehicle_status;                      // Function pointer to get the current status of the vehicle  
16        std::function<VehicleConstants()> get_vehicle_constants;                // Function pointer to get the current dynamical constants of the vehicle    
17        std::function<void()> signal_mode_finished;                             // Function pointer to signal that the mode has finished operating
18        Controller::SharedPtr controller;                                       // Controller to be used by the mode
19        TrajectoryManager::SharedPtr trajectory_manager;                        // Trajectory manager that can be used by some modes
20    };
21
22    // Custom constructor like function - as we must have the default constructor for the pluginlib
23    inline void initialize_mode(const Mode::Config & config) {
24
25        // Initialize the base class
26        node_ = config.node;
27        get_vehicle_state = config.get_vehicle_state;
28        get_vehicle_status = config.get_vehicle_status;
29        get_vehicle_constants = config.get_vehicle_constants;        
30        signal_mode_finished = config.signal_mode_finished;
31
32        // Initialize the controller
33        controller_ = config.controller;
34
35        // Initialize the trajectory manager
36        trajectory_manager_ = config.trajectory_manager;
37
38        // Initialize the derived class
39        initialize();
40    }
41
42    // Methods that can be implemented by derived classes
43    // that are executed by the state machine when entering, exiting or updating the mode
44    virtual void initialize() = 0;
45    virtual bool enter() = 0;
46    virtual bool exit() = 0;
47    virtual void update(double dt) = 0;
48
49protected:
50
51    // The ROS 2 node
52    rclcpp::Node::SharedPtr node_{nullptr};
53
54    // The controller object that can be used by each mode to track references
55    Controller::SharedPtr controller_{nullptr};
56
57    // The trajectory manager object that can be used by each mode to generate references to track
58    TrajectoryManager::SharedPtr trajectory_manager_{nullptr};
59
60    // Function pointer which will be instantiated with the function pointers passed in the configuration
61    std::function<State()> get_vehicle_state{nullptr};
62    std::function<VehicleStatus()> get_vehicle_status{nullptr};
63    std::function<VehicleConstants()> get_vehicle_constants{nullptr};
64
65    // Function pointer to signal that the mode has finished operating
66    std::function<void()> signal_mode_finished{nullptr};
67};
68
69} // namespace autopilot

Additionally, the Mode class provides methods to get:

  1. The current State of the vehicle can be accessed by calling get_vehicle_state() . It returns the following struct:

1namespace autopilot {
2// State of the vehicle
3struct State {
4    Eigen::Vector3d position{Eigen::Vector3d::Zero()};           // Position of the vehicle (FRD) in the world frame NED
5    Eigen::Vector3d velocity{Eigen::Vector3d::Zero()};           // Velocity of the vehicle (FRD) with respect to the world frame NED expressed in the world frame NED
6    Eigen::Quaterniond attitude{1.0, 0.0, 0.0, 0.0};             // Attitude of the vehicle (FRD) with respect to the world frame NED expressed in the world frame NED
7    Eigen::Vector3d angular_velocity{Eigen::Vector3d::Zero()};   // Angular velocity of the vehicle (FRD) with respect to the world frame NED expressed in the body frame FRD
8};
9}
  1. The Status of the vehicle can be accessed by calling get_vehicle_status() . It returns the following struct:

1namespace autopilot {
2// Status of the vehicle
3struct VehicleStatus {
4    bool armed{false};      // Whether the vehicle is armed
5    bool flying{false};     // Whether the vehicle is flying
6    bool offboard{false};   // Whether the vehicle is in offboard mode
7};
8}
  1. The Constants such as mass or thrust curve can be accessed by calling get_vehicle_constants . It returns the following struct:

1namespace autopilot {
2// Dynamical constants of the vehicle
3struct VehicleConstants {
4    double mass{0.0};                                                         // Mass of the vehicle (in Kg)
5    std::string thrust_curve_id{"None"};                                      // Thrust curve of the vehicle
6    std::vector<std::string> thurst_curve_params{std::vector<std::string>()}; // Thrust curve parameters
7    std::vector<double> thrust_curve_values{std::vector<double>()};           // Thrust curve values associated with the parameters
8};
9}

This means that to access these values, you do not need to manually subscribe to the ROS 2 topics. The autopilot takes care of that for you. However, if you are required to access other values that are not provided by the Mode class, or if you need to publish values to the ROS 2 topics, you can do so by using the node_ shared pointer that is a member of the Mode class.

4. The Mode class also provides a shared pointer to a controller_ object that can be used to access the controller interface. To check the available methods in the controller interface, please refer to the Controllers page. This is useful if you want to send control commands to the vehicle without having to publish to the ROS 2 topics directly. The autopilot takes care of that for you.

5. The Mode class also provides a shared pointer to a trajectory_manager_ object that can be used to access the trajectory manager interface. To check the available methods in the trajectory manager interface, please refer to the Trajectory Manager page. This is useful if your mode needs to follow a trajectory and you don’t want to manually subscribe to the ROS 2 topics or hard-code the trajectory in the mode.

1. Provided State Machine Modes

The autopilot provides the following operating modes:

  • Arm - The vehicle is armed and ready to take off.

  • Disarm - The vehicle is on the ground and disarmed.

  • Takeoff - The vehicle takes of to a predefined altitude above the current position.

  • Land - The vehicle lands at the current position.

  • Hold - The vehicle holds its current position.

  • Follow Trajectory - The vehicle follows the trajectory loaded in the trajectory manager.

  • Waypoint - The vehicle goes to a waypoint.

Since the autopilot modes are implemented as plugins, they are loaded at runtime by the autopilot. The autopilot uses the pluginlib package to load the plugins. This also allows for the modes to be implemented in different packages, which is useful if you want to keep the autopilot package clean and modular.

For instance, if you want to check the implementation of the provided base modes, you can find them in the pegasus_autopilot/autopilot_modes package.

2. Adding Custom Modes to the State Machine

In this section we will implement a custom waypoint operating mode. In this mode, we will subscribe to a ROS 2 topic that publishes the desired waypoint and drive the vehicle to that waypoint. This is a simple example to show how to implement a custom operating mode. You can implement more complex modes by following the same steps.

  1. Start by creating a new ROS 2 package inside your workspace, where you will implement the custom operating mode.

# Go to the src directory of your workspace
cd ~/pegasus/src

# Create a C++ ROS 2 package where you are going to implement the custom modes
ros2 pkg create --build-type ament_cmake custom_modes

# Go inside the package directory
cd custom_modes

# Create a xml file where you are going to define your custom mode as a plugin
touch custom_modes_plugins.xml
  1. Inside the include/custom_modes directory, create a new header file named mode_custom_waypoint.hpp.

# Create a new header file inside the include directory of the custom_modes package
touch include/custom_modes/mode_custom_waypoint.hpp

This file will contain the definition of the custom operating mode that drives the vehicle to a pre-defined waypoint. Your operation mode must inherit from the autopilot::Mode class and implement the following methods: initialize, enter, exit and update.

 1#pragma once
 2#include <Eigen/Core>
 3#include <autopilot/mode.hpp>
 4#include "pegasus_msgs/msg/control_position.hpp"
 5
 6#include "rclcpp/rclcpp.hpp"
 7
 8namespace autopilot {
 9
10class CustomWaypointMode : public autopilot::Mode {
11
12public:
13
14   ~CustomWaypointMode();
15   void initialize() override;
16   bool enter() override;
17   bool exit() override;
18   void update(double dt) override;
19
20protected:
21
22   // ROS 2 subscriber for the waypoint topic
23   rclcpp::Subscription<pegasus_msgs::msg::ControlPosition>::SharedPtr waypoint_subscriber_;
24
25   // Callback for the waypoint subscriber
26   void waypoint_callback(const pegasus_msgs::msg::ControlPosition::ConstSharedPtr msg);
27
28   // Stores the latest target position and attitude waypoint to be at
29   Eigen::Vector3d target_pos{1.0, 1.0, -1.5};
30   float target_yaw{0.0f};
31   bool waypoint_set_{false};
32};
33}
  • The initialize is called by the autopilot when it is loading all the operating modes into memory. This is where you should initialize the ROS 2 publishers, subscribers and services if needed.

  • The enter is called by the autopilot when the mode is about to be entered. This is where you should check if the mode can be entered and set the necessary flags. If the mode cannot be entered, return false, and the autopilot will not enter this mode and will keep the current operation mode.

  • The exit is called by the autopilot when the mode is about to be exited. This is where you should reset the flags and clean up any resources that were allocated.

  • The update is called by the autopilot at every iteration of the control loop. This is where you should implement the logic of the mode.

  1. Inside the src directory, create a new source file named mode_custom_waypoint.cpp.

# Go to the src directory of the custom_modes package
touch src/mode_custom_waypoint.cpp

This file will contain the implementation of the custom operating mode that drives the vehicle to a given waypoint.

 1#include "custom_modes/mode_custom_waypoint.hpp"
 2
 3namespace autopilot {
 4
 5CustomWaypointMode::~CustomWaypointMode() {
 6   // Terminate the waypoint subscriber
 7   this->waypoint_subscriber_.reset();
 8}
 9
10// This method is called when the autopilot loads all the modes into memory
11void CustomWaypointMode::initialize() {
12
13   // Get the name of the topic where the waypoint is published from the ROS 2 parameter server
14   node_->declare_parameter<std::string>("autopilot.CustomWaypointMode.waypoint_topic", "waypoint");
15
16   // Create a subscriber to the waypoint topic (to get the desired waypoint to track)
17   this->waypoint_subscriber_ = this->node_->create_subscription<pegasus_msgs::msg::ControlPosition>(
18      node_->get_parameter("autopilot.CustomWaypointMode.waypoint_topic").as_string(),
19      rclcpp::SystemDefaultsQoS(),
20      std::bind(&CustomWaypointMode::waypoint_callback, this, std::placeholders::_1));
21
22   // Log that the waypoint mode has been initialized correctly
23   RCLCPP_INFO(this->node_->get_logger(), "CustomWaypointMode initialized");
24}
25
26// This method is called when the autopilot is about to enter the waypoint mode
27bool CustomWaypointMode::enter() {
28
29   // Check if the waypoint was already set - if not, then do not enter the waypoint mode
30   if (!this->waypoint_set_) {
31      RCLCPP_ERROR(this->node_->get_logger(), "Waypoint not set - cannot enter waypoint mode.");
32      return false;
33   }
34
35   // Reset the waypoint flag (to make sure we do not enter twice in this mode without setting a new waypoint)
36   this->waypoint_set_ = false;
37
38   // Return true to indicate that the mode has been entered successfully
39   return true;
40}
41
42// This method is called when the autopilot is about to exit the waypoint mode and enter another mode
43bool CustomWaypointMode::exit() {
44
45   this->waypoint_set_ = false;
46
47   // Nothing to do here
48   return true;   // Return true to indicate that the mode has been exited successfully
49}
50
51// This method is called at every iteration of the control loop by the autopilot
52void CustomWaypointMode::update(double dt) {
53
54   // Set the controller to track the target position and attitude
55   // In this case we do not which to implement a low-level controller, so we will use the set_position method from the controller interface
56   // and use whatever controller is currently active in the autopilot.
57   // Note: we could also have a custom implementation here and send lower level controls using the controller interface
58   this->controller_->set_position(this->target_pos, this->target_yaw, dt);
59}
60
61// ROS 2 callback for the waypoint subscriber - this is called whenever a new waypoint is published
62void CustomWaypointMode::waypoint_callback(const pegasus_msgs::msg::ControlPosition::ConstSharedPtr msg) {
63
64   // Set the waypoint
65   this->target_pos[0] = msg->position[0];
66   this->target_pos[1] = msg->position[1];
67   this->target_pos[2] = msg->position[2];
68   this->target_yaw = msg->yaw;
69
70   // Set the waypoint flag
71   this->waypoint_set_ = true;
72
73   // Log that the waypoint has been set successfully
74   RCLCPP_WARN(this->node_->get_logger(), "Waypoint set to (%f, %f, %f) with yaw %f", this->target_pos[0], this->target_pos[1], this->target_pos[2], this->target_yaw);
75}
76
77} // namespace autopilot
78
79#include <pluginlib/class_list_macros.hpp>
80PLUGINLIB_EXPORT_CLASS(autopilot::CustomWaypointMode, autopilot::Mode)

The last 2 lines of the code snippet above are necessary to export the custom mode as a plugin. The pluginlib package uses these lines to load the plugin at runtime.

4. Modify the package.xml file to include the following dependencies autopilot and pluginlib, according to the code snippet below. Additionally, we also need to include the pegasus_msgs package as a dependency, since we are subscribing to a ROS 2 topic that publishes messages from this package.

 1<?xml version="1.0"?>
 2<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
 3<package format="3">
 4   <name>custom_modes</name>
 5   <version>1.0.0</version>
 6   <description></description>
 7   <author email="your.email@tecnico.ulisboa.pt">Author Name</author>
 8   <license>BSD-3</license>
 9
10   <buildtool_depend>ament_cmake_ros</buildtool_depend>
11
12   <depend>autopilot</depend>
13   <depend>pluginlib</depend>
14   <depend>pegasus_msgs</depend>
15
16   <test_depend>ament_lint_auto</test_depend>
17   <test_depend>ament_lint_common</test_depend>
18
19   <export>
20      <build_type>ament_cmake</build_type>
21   </export>
22</package>
  1. Also modify the CMakeLists.txt file to include the following dependencies autopilot, pegasus_msgs and pluginlib, according to the code snippet below. Additionally, we need to include the Eigen3 package as a dependency, since we are using Eigen for linear algebra operations.

 1cmake_minimum_required(VERSION 3.8)
 2project(custom_modes)
 3
 4# Default to C++20 and compiler flags to give all warnings
 5set(CMAKE_CXX_STANDARD 20)
 6set(CMAKE_CXX_STANDARD_REQUIRED ON)
 7
 8if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
 9add_compile_options(-Wall -Wextra -Wpedantic -Wno-unused-parameter -Wno-sign-compare -O3)
10endif()
11
12# find dependencies
13find_package(ament_cmake REQUIRED)
14find_package(autopilot REQUIRED)
15find_package(pluginlib REQUIRED)
16find_package(pegasus_msgs REQUIRED)
17find_package(Eigen3 REQUIRED)
18
19add_library(${PROJECT_NAME}
20   src/mode_custom_waypoint.cpp
21)
22
23target_include_directories(${PROJECT_NAME} PUBLIC
24   $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
25   $<INSTALL_INTERFACE:include>
26   ${EIGEN3_INCLUDE_DIRS}
27)
28
29add_definitions(${EIGEN3_DEFINITIONS})
30
31set(dependencies
32   autopilot
33   pluginlib
34   pegasus_msgs
35)
36
37ament_target_dependencies(${PROJECT_NAME} ${dependencies})
38
39# Export the pluginlib description (package containing the base class and the derived classes information in XML format)
40pluginlib_export_plugin_description_file(autopilot custom_modes_plugins.xml)
41
42install(
43   TARGETS ${PROJECT_NAME}
44   EXPORT export_${PROJECT_NAME}
45   ARCHIVE DESTINATION lib
46   LIBRARY DESTINATION lib
47   RUNTIME DESTINATION bin
48)
49
50# Causes the visibility macros to use dllexport rather than dllimport,
51# which is appropriate when building the dll but not consuming it.
52target_compile_definitions(${PROJECT_NAME} PRIVATE "CUSTOM_MODES_BUILDING_LIBRARY")
53
54install(
55   DIRECTORY include/
56   DESTINATION include
57)
58
59if(BUILD_TESTING)
60find_package(ament_lint_auto REQUIRED)
61# the following line skips the linter which checks for copyrights
62# comment the line when a copyright and license is added to all source files
63set(ament_cmake_copyright_FOUND TRUE)
64# the following line skips cpplint (only works in a git repo)
65# comment the line when this package is in a git repo and when
66# a copyright and license is added to all source files
67set(ament_cmake_cpplint_FOUND TRUE)
68ament_lint_auto_find_test_dependencies()
69endif()
70
71ament_export_include_directories(include)
72ament_export_libraries(${PROJECT_NAME})
73ament_export_dependencies(${dependencies})
74ament_export_targets(export_${PROJECT_NAME})
75ament_package()
  1. Create a xml file named custom_modes_plugins.xml inside the package directory. This file will contain the definition of the custom mode as a plugin.

# Create a xml file inside the package directory
touch custom_modes_plugins.xml

The content of the custom_modes_plugins.xml file should be as follows:

1<library path="custom_modes">
2   <class type="autopilot::CustomWaypointMode" base_class_type="autopilot::Mode">
3      <description>A custom waypoint mode that drives the vehicle to a given waypoint.</description>
4   </class>
5</library>

7. Create a configuration file named custom_modes.yaml inside the package directory. This file will contain the configuration parameters for launching the autopilot with the custom mode.

# Create a yaml file inside the package directory
touch config/custom_modes.yaml

The content of the custom_modes.yaml file should be as follows. In line 8, we define the modes that will be loaded by the autopilot. In this case, we are adding the custom waypoint mode to the list of modes.

Lines 23-26 define the configuration parameters for each mode, including the newly created custom waypoint mode. In this case, we are defining the topic where the waypoint is published.

We also define the valid_transitions, which define which modes are allowed to transition to from the custom waypoint mode. In this case, the custom waypoint mode can transition to the Hold and Land modes. Moreover we define the fallback mode, which is the mode that the autopilot will transition to if the custom waypoint mode is not able to execute its logic.

 1/**:
 2ros__parameters:
 3   autopilot:
 4      # Update rate
 5      rate: 50.0 # Hz
 6      default_mode: "DisarmMode"
 7      # Define all the existing operation modes
 8      modes: ["DisarmMode", "ArmMode", "LandMode", "HoldMode", "CustomWaypointMode"]
 9      # Configurations of each operating mode:
10      DisarmMode:
11         valid_transitions: ["ArmMode"]
12         fallback: "DisarmMode"
13         disarm_service: "fmu/kill_switch"
14      ArmMode:
15         valid_transitions: ["DisarmMode", "CustomWaypointMode"]
16         fallback: "DisarmMode"
17         geofencing_violation_fallback: "DisarmMode"
18         arm_service: "fmu/arm"
19         offboard_service: "fmu/offboard"
20      HoldMode:
21         valid_transitions: ["LandMode", "CustomWaypointMode"]
22         fallback: "LandMode"
23      LandMode:
24         valid_transitions: ["DisarmMode", "ArmMode", "TakeoffMode", "HoldMode", "WaypointMode", "FollowTrajectoryMode", "PassThroughMode"]
25         fallback: "HoldMode"
26         on_finish: "DisarmMode"
27         land_speed: 0.2 # m/s
28         land_detected_treshold: 0.1 # m/s
29         countdown_to_disarm: 3.0 # s
30      CustomWaypointMode:
31         valid_transitions: ["HoldMode", "LandMode"]
32         fallback: "HoldMode"
33         waypoint_topic: "waypoint"
34      # Topics configurations for the autopilot to communicate with the rest of the system
35      publishers:
36         control_position: "fmu/in/position"
37         control_attitude: "fmu/in/force/attitude"
38         control_attitude_rate: "fmu/in/force/attitude_rate"
39         status: "autopilot/status"
40      subscribers:
41         state: "fmu/filter/state"
42         status: "fmu/status"
43         constants: "fmu/constants"
44      services:
45         set_mode: "autopilot/change_mode"
46      # ----------------------------------------------------------------------------------------------------------
47      # Definition of the controller that will perform the tracking of references of the different operation modes
48      # ----------------------------------------------------------------------------------------------------------
49      controller: "OnboardController"
50      OnboardController:
51         publishers:
52            control_position: "fmu/in/position"
53            control_attitude: "fmu/in/force/attitude"
54            control_attitude_rate: "fmu/in/force/attitude_rate"
55      # ----------------------------------------------------------------------------------------------------------
56      # Definition of the geofencing mechanism that will keep the vehicle in safe places
57      # ----------------------------------------------------------------------------------------------------------
58      geofencing: "BoxGeofencing"
59      BoxGeofencing:
60         limits_x: [-10.0, 10.0]
61         limits_y: [-10.0, 10.0]
62         limits_z: [-10.0,  1.0] # NED Coordinades (z-negative is up)
63      # ----------------------------------------------------------------------------------------------------------
64      # Definition of the trajectory manager that generates parameterized trajectories to be followed
65      # ----------------------------------------------------------------------------------------------------------
66      trajectory_manager: "StaticTrajectoryManager"
67      StaticTrajectoryManager:
68         services:
69            reset_trajectory: "autopilot/trajectory/reset"
  1. Create a launch file named custom_modes.launch.py inside the package directory. This file will launch the autopilot with the configurations provided in the custom_modes.yaml file.

# Create a launch file inside the package directory
touch launch/custom_modes.launch.py

The content of the custom_modes.launch.py file should be as follows:

 1from launch import LaunchDescription
 2from launch_ros.actions import Node
 3from launch.actions import DeclareLaunchArgument
 4from launch.substitutions import LaunchConfiguration
 5
 6def generate_launch_description():
 7   return LaunchDescription([
 8      DeclareLaunchArgument(
 9         'config_file',
10         default_value='custom_modes.yaml',
11         description='Path to the configuration file for the autopilot'
12      ),
13      Node(
14         package='autopilot',
15         executable='autopilot_node',
16         name='autopilot',
17         output='screen',
18         parameters=[LaunchConfiguration('config_file')]
19      )
20   ])