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:
Section 0. Mode Interface introduces the “Mode” class that defines the interface for the autopilot operating modes.
Section 1. Provided State Machine Modes provides an overview of the provided operating modes and where they are implemented.
Section 2. Adding Custom Modes to the State Machine provides a step-by-step guide on how to add custom operating modes to the state machine.
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:
The current
State
of the vehicle can be accessed by callingget_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};
The
Status
of the vehicle can be accessed by callingget_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};
The
Constants
such as mass or thrust curve can be accessed by callingget_vehicle_constants
. It returns the following struct:
1namespace autopilot {
2// Dynamical constants of the vehicle
3struct VehicleConstants {
4 int id{0}; // ID of the vehicle
5 double mass{0.0}; // Mass of the vehicle (in Kg)
6 std::string thrust_curve_id{"None"}; // Thrust curve of the vehicle
7 std::vector<std::string> thurst_curve_params{std::vector<std::string>()}; // Thrust curve parameters
8 std::vector<double> thrust_curve_values{std::vector<double>()}; // Thrust curve values associated with the parameters
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.
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
Inside the
include/custom_modes
directory, create a new header file namedmode_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.
Inside the
src
directory, create a new source file namedmode_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>
Also modify the
CMakeLists.txt
file to include the following dependenciesautopilot
,pegasus_msgs
andpluginlib
, according to the code snippet below. Additionally, we need to include theEigen3
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()
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"
Create a launch file named
custom_modes.launch.py
inside the package directory. This file will launch the autopilot with the configurations provided in thecustom_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 ])