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 ])