Table of Contents
- Introduction
- Setup the environment
- Dynamic Model
- Problem Statement
- Packages used
- Design Details
- License
The objective of this project is to develop a robust control scheme to enable a quadrotor to track desired trajectories in the presence of external disturbances.
The control design under study will be tested on the Crazyflie 2.0 platform. Crazyflie is a quadrotor that is classified as a micro air vehicle (MAV), as it only weighs 27 grams and can fit in your hand. The size makes it ideal for flying inside a lab without trashing half the interior. Even though the propellers spin at high RPMs, they are soft and the torque in the motors is very low when compared to a brushless motor, making it relatively crash tolerant. The Crazyflie 2.0 features four 7mm coreless DC-motors that give the Crazyflie a maximum takeoff weight of 42g.
The Crazyflie 2.0 is an open source project, with source code and hardware design both documented and available. For more information, see the link below: https://www.bitcraze.io/products/old-products/crazyflie-2-0/
Crazyflie 2.0 Quadrotor Hardware
Crazyflie 2.0 Quadrotor Gazebo Simulation
Note: The Complete project is build and tested on Ubuntu 20.04LTS with ROS Noetic and not tested on any other system. Kindly proceed with caution and loads of google usage for porting it to any other system. And as always, common sense is must before anything!!
-
Setup your sources.list Setup your computer to accept software from packages.ros.org.
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
-
Set up your keys
sudo apt install curl # if you haven't already installed curl curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
-
Installation First, make sure your Debian package index is up-to-date:
sudo apt update
Desktop-Full Install: (Recommended) : Everything in Desktop plus 2D/3D simulators and 2D/3D perception packages
sudo apt install ros-noetic-desktop-full
-
Environment setup You must source this script in every bash terminal you use ROS in.
source /opt/ros/noetic/setup.bash
It can be convenient to automatically source this script every time a new shell is launched. These commands will do that for you.
echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc source ~/.bashrc
-
Dependencies for building packages Up to now you have installed what you need to run the core ROS packages. To create and manage your own ROS workspaces, there are various tools and requirements that are distributed separately. For example, rosinstall is a frequently used command-line tool that enables you to easily download many source trees for ROS packages with one command.
To install this tool and other dependencies for building ROS packages, run:
sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential
Initialize rosdep
Before you can use many ROS tools, you will need to initialize rosdep. rosdep enables you to easily install system dependencies for source you want to compile and is required to run some core components in ROS. If you have not yet installed rosdep, do so as follows.
sudo apt install python3-rosdep
With the following, you can initialize rosdep.
sudo rosdep init rosdep update
-
Create a workspace
mkdir -p ~/rbe502_ros/src cd ~/rbe502_ros catkin_make echo "source ~/rbe502_ros/devel/setup.bash" >> ~/.bashrc source ~/rbe502_ros/devel/setup.bash
-
Install the Control and Effort Dependencies for Gazebo
sudo apt-get update sudo apt-get upgrade sudo apt update sudo apt-get install ros-noetic-ros-control ros-noetic-ros-controllers sudo apt-get install ros-noetic-gazebo-ros-pkgs ros-noetic-gazebo-ros-control
-
With all dependencies ready, we can build the ROS package by the following commands.
cd ~/rbe502_ros catkin_make
To set up the Crazyflie 2.0 quadrotor in Gazebo, we need to install additional ROS dependencies for building packages as below:
sudo apt update
sudo apt install ros-noetic-joy ros-noetic-octomap-ros ros-noetic-mavlink
sudo apt install ros-noetic-octomap-mapping ros-noetic-control-toolbox
sudo apt install python3-vcstool python3-catkin-tools protobuf-compiler libgoogle-glog-dev
rosdep update
sudo apt-get install ros-noetic-ros libgoogle-glog-dev
We are now ready to create a new ROS workspace and download the ROS packages for the robot:
mkdir -p ~/rbe502_project/src
cd ~/rbe502_project/src
catkin_init_workspace # initialize your catkin workspace
cd ~/rbe502_project
catkin init
catkin build -j1
cd ~/rbe502_project/src
git clone -b dev/ros-noetic https://github.com/gsilano/CrazyS.git
git clone -b med18_gazebo9 https://github.com/gsilano/mav_comm.git
Note: a new ROS workspace is needed for the project, because the CrazyS Gazebo package is built using the catkin build
tool, instead of catkin_make
.
Note: -j1 in catkin build is for safety so it does not cause you computer to hang. It makes your code to build on just one core. Slow but ensures it compiles without issues
We need to build the project workspace using python_catkin_tools
, therefore we need to configure it:
cd ~/rbe502_project
rosdep install --from-paths src -i
rosdep update
catkin config --cmake-args -DCMAKE_BUILD_TYPE=Release -DCATKIN_ENABLE_TESTING=False
catkin build -j1
This is gonna take a lot of time. Like a real lot. So maybe make yourself a cup of coffee meanwhile? If you don't like coffee, I don't know your motivation to live 😟
Do not forget to add sourcing to your .bashrc
file:
echo "source ~/rbe502_project/devel/setup.bash" >> ~/.bashrc
source ~/.bashrc
With all dependencies ready, we can build the ROS package by the following commands:
cd ~/rbe502_project
catkin build -j1
To spawn the quadrotor in Gazebo, we can run the following launch file:
roslaunch rotors_gazebo crazyflie2_without_controller.launch
Congrats, All the setup is done for the simulation to start. You can start writing your own algorithm if you want to!! 😃
The quadrotor model is shown below.
Considering two coordinate frames specifically the world coordinate frame -
with the translational coordinates
The control inputs on the system can be considered simply as:
where
For a set of desired control inputs, the desired rotor speeds (i.e.
where
Considering the generalized coordinates and the control inputs defined above, the simplified equations of motion (assuming small angles) for the translational accelerations and body frame angular accelerations are derived as:
where
The physical parameters for the Crazyflie 2.0 hardware are listed below
Parameter | Symbol | Value |
---|---|---|
Quadrotor mass | ||
Quadrotor arm length | ||
Quadrotor inertia along x-axis | ||
Quadrotor inertia along y-axis | ||
Quadrotor inertia along z-axis | ||
Propeller moment of Inertia | ||
Propeller thrust factor | ||
Propeller moment factor | ||
Rotor maximum speed | ||
Rotor minimum speed |
Remark 1: As shown in the equations of motion above, the quadrotor system has six DoF, with only four control inputs. As a result, the control of quadrotors is typically done by controlling only the altitude
Design a sliding mode controller for altitude and attitude control of the Crazyflie 2.0 to enable the quadrotor to track desired trajectories and visit a set of desired waypoints.
The main components of the project are described below.
Write a MATLAB or Python script to generate quintic (fifth-order) trajectories (position, velocity and acceleration) for the translational coordinates
-
$p_{0} = (0, 0, 0)$ to$p_{1} = (0, 0, 1)$ in 5 seconds -
$p_{1} = (0, 0, 1)$ to$p_{2} = (1, 0, 1)$ in 15 seconds -
$p_{2} = (1, 0, 1)$ to$p_{3} = (1, 1, 1)$ in 15 seconds -
$p_{3} = (1, 1, 1)$ to$p_{4} = (0, 1, 1)$ in 15 seconds -
$p_{4} = (0, 1, 1)$ to$p_{5} = (0, 0, 1)$ in 15 seconds
The sequence of visiting the waypoints does matter. The velocity and acceleration at each waypoint must be equal to zero.
Solution:
A quintic (fifth-order) equation is used for trajectory solution.
The
Thus, the coefficients can be calculated using:
Using this, the resulting equations of motion are:
Trajectory 1:
Trajectory 2:
Trajectory 3:
Trajectory 4:
Trajectory 5:
The resultant trajectories will look as shown:
Code:
The complete trajectory coordinates are generated by calling the traj_evaluate()
function. It takes the self.t
as input for the current time and based of some conditional statements to evaulate the correct trajectories with respect to current time, it returns the
def traj_evaluate(self):
# Switching the desired trajectory according to time
if self.t <= 70:
if self.t <= 5:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
0, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, self.t)
print(f'Lift OFF - T:{round(self.t,3)}')
elif self.t <= 20:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
5, 20, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, self.t)
print(f'Path 1 - T:{round(self.t,3)}')
elif self.t <= 35:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
20, 35, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, self.t)
print(f'Path 2 - T:{round(self.t,3)}')
elif self.t <= 50:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
35, 50, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, self.t)
print(f'Path 3 - T:{round(self.t,3)}')
elif self.t <= 65:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
50, 65, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, self.t)
print(f'Path 4 - T:{round(self.t,3)}')
else:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
65, 70, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, self.t)
print(f'Landing - T:{round(self.t,3)}')
A = np.array([[1, t0, t0**2, t0**3, t0**4, t0**5],
[0, 1, 2*t0, 3*t0**2, 4*t0**3, 5*t0**4],
[0, 0, 2, 6*t0, 12*t0**2, 20*t0**3],
[1, tf, tf**2, tf ** 3, tf ** 4, tf ** 5],
[0, 1, 2*tf, 3 * tf ** 2, 4 * tf ** 3, 5 * tf ** 4],
[0, 0, 2, 6 * tf, 12 * tf ** 2, 20 * tf ** 3]])
bx = np.array(([x0], [v0], [ac0], [xf], [vf], [acf]))
by = np.array(([y0], [v0], [ac0], [yf], [vf], [acf]))
bz = np.array(([z0], [v0], [ac0], [zf], [vf], [acf]))
ax = np.dot(np.linalg.inv(A), bx)
ay = np.dot(np.linalg.inv(A), by)
az = np.dot(np.linalg.inv(A), bz)
# Position
xd = ax[0] + ax[1]*t + ax[2]*t**2 + \
ax[3]*t**3 + ax[4]*t**4 + ax[5]*t**5
yd = ay[0] + ay[1]*t + ay[2]*t**2 + \
ay[3]*t**3 + ay[4]*t**4 + ay[5]*t**5
zd = az[0] + az[1]*t + az[2]*t**2 + \
az[3]*t**3 + az[4]*t**4 + az[5]*t**5
# Velocity
xd_dot = ax[1] + 2*ax[2]*t + 3*ax[3] * \
t**2 + 4*ax[4]*t**3 + 5*ax[5]*t**4
yd_dot = ay[1] + 2*ay[2]*t + 3*ay[3] * \
t**2 + 4*ay[4]*t**3 + 5*ay[5]*t**4
zd_dot = az[1] + 2*az[2]*t + 3*az[3] * \
t**2 + 4*az[4]*t**3 + 5*az[5]*t**4
# Acceleration
xd_ddot = 2*ax[2] + 6*ax[3]*t + 12*ax[4]*t**2 + 20*ax[5]*t**3
yd_ddot = 2*ay[2] + 6*ay[3]*t + 12*ay[4]*t**2 + 20*ay[5]*t**3
zd_ddot = 2*az[2] + 6*az[3]*t + 12*az[4]*t**2 + 20*az[5]*t**3
else:
xd, yd, zd, xd_dot, yd_dot, zd_dot, xd_ddot, yd_ddot, zd_ddot = (
0, 0, 0, 0, 0, 0, 0, 0, 0)
print(f'Landed - T:{round(self.t,3)}')
rospy.signal_shutdown("Trajectory Tracking Complete")
return xd, yd, zd, xd_dot, yd_dot, zd_dot, xd_ddot, yd_ddot, zd_ddot
The conditional statements provide the trajectories between two points based on current time. It returns trajectory start and end point as follows:
Each condition is used to set the values of initial and final variables for trajectory evaulation which include time, position, velocity and acceleration.
if self.t <= 5:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
0, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, self.t)
print(f'Lift OFF - T:{round(self.t,3)}')
elif self.t <= 20:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
5, 20, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, self.t)
print(f'Path 1 - T:{round(self.t,3)}')
elif self.t <= 35:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
20, 35, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, self.t)
print(f'Path 2 - T:{round(self.t,3)}')
elif self.t <= 50:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
35, 50, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, self.t)
print(f'Path 3 - T:{round(self.t,3)}')
elif self.t <= 65:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
50, 65, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, self.t)
print(f'Path 4 - T:{round(self.t,3)}')
else:
t0, tf, x0, y0, z0, xf, yf, zf, v0, vf, ac0, acf, t = (
65, 70, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, self.t)
print(f'Landing - T:{round(self.t,3)}')
Using the initial and final times, the time matrix is generated. This is generated by substituting the initial and final time values in quintic polynomial equation for position and its derivative and double derivative for velocity and acceleration respectively.
A = np.array([[1, t0, t0**2, t0**3, t0**4, t0**5],
[0, 1, 2*t0, 3*t0**2, 4*t0**3, 5*t0**4],
[0, 0, 2, 6*t0, 12*t0**2, 20*t0**3],
[1, tf, tf**2, tf ** 3, tf ** 4, tf ** 5],
[0, 1, 2*tf, 3 * tf ** 2, 4 * tf ** 3, 5 * tf ** 4],
[0, 0, 2, 6 * tf, 12 * tf ** 2, 20 * tf ** 3]])
The b
matrix is the RHS matrix which holds the value of initial and final position, velocity and acceleration as explained above in the math.
bx = np.array(([x0], [v0], [ac0], [xf], [vf], [acf]))
by = np.array(([y0], [v0], [ac0], [yf], [vf], [acf]))
bz = np.array(([z0], [v0], [ac0], [zf], [vf], [acf]))
Finally, the coefficients of quintic polynomial are calculated by multiplying the inverse of time matrix
with b
matrix
ax = np.dot(np.linalg.inv(A), bx)
ay = np.dot(np.linalg.inv(A), by)
az = np.dot(np.linalg.inv(A), bz)
Finally the quintic polynomial for is reconstructed using the coefficient and current time. This generates
# Position
xd = ax[0] + ax[1]*t + ax[2]*t**2 + \
ax[3]*t**3 + ax[4]*t**4 + ax[5]*t**5
yd = ay[0] + ay[1]*t + ay[2]*t**2 + \
ay[3]*t**3 + ay[4]*t**4 + ay[5]*t**5
zd = az[0] + az[1]*t + az[2]*t**2 + \
az[3]*t**3 + az[4]*t**4 + az[5]*t**5
Similarly
# Velocity
xd_dot = ax[1] + 2*ax[2]*t + 3*ax[3] * \
t**2 + 4*ax[4]*t**3 + 5*ax[5]*t**4
yd_dot = ay[1] + 2*ay[2]*t + 3*ay[3] * \
t**2 + 4*ay[4]*t**3 + 5*ay[5]*t**4
zd_dot = az[1] + 2*az[2]*t + 3*az[3] * \
t**2 + 4*az[4]*t**3 + 5*az[5]*t**4
Similarly
# Acceleration
xd_ddot = 2*ax[2] + 6*ax[3]*t + 12*ax[4]*t**2 + 20*ax[5]*t**3
yd_ddot = 2*ay[2] + 6*ay[3]*t + 12*ay[4]*t**2 + 20*ay[5]*t**3
zd_ddot = 2*az[2] + 6*az[3]*t + 12*az[4]*t**2 + 20*az[5]*t**3
In the condition when the complete trajectory is traced, the origin is returned with zero velocity and acceleration to power off the drone.
else:
xd, yd, zd, xd_dot, yd_dot, zd_dot, xd_ddot, yd_ddot, zd_ddot = (
0, 0, 0, 0, 0, 0, 0, 0, 0)
print(f'Landed - T:{round(self.t,3)}')
After trajectory tracking is complete, the ros node is shutdown as there is no further path left to trace.
rospy.signal_shutdown("Trajectory Tracking Complete")
The complete function returns position, velocity and acceleration
return xd, yd, zd, xd_dot, yd_dot, zd_dot, xd_ddot, yd_ddot, zd_ddot
Considering the equations of motion provided above, design boundary layer-based sliding mode control laws for the
Remark 2: To convert the desired position trajectories
Remark 3: For the purpose of this assignment, the desired yaw angle
The resulting discrepancy can be considered as an external disturbance that is handled through the robust control design in this assignment.
Remark 4: When designing the sliding mode control laws, assume that all the model parameters are known. In fact, the objective of this assignment is to design a sliding mode controller to be robust under reasonable external disturbances
Solution: Four sliding mode controller will be needed to control the drone.
where,
where,
Also,
Placing
Making the coefficient of
The control strategy we came up to is to design a controller which cancels the system dynamics:
where,
Placing equation
We are designing the controller with reduced chattering, so we shall be adding a boundary layer of
for
where,
where,
Also,
Placing
Making the coefficient of
The control strategy we came up to is to design a controller which cancels the system dynamics:
where,
Placing equation
We are designing the controller with reduced chattering, so we shall be adding a boundary layer of
for
where,
where,
Also,
Placing
Making the coefficient of
The control strategy we came up to is to design a controller which cancels the system dynamics:
where,
Placing equation
We are designing the controller with reduced chattering, so we shall be adding a boundary layer of
for
where,
where,
Also,
Placing
Making the coefficient of
The control strategy we came up to is to design a controller which cancels the system dynamics:
where,
Placing equation
We are designing the controller with reduced chattering, so we shall be adding a boundary layer of
for
The complete system is accurately tuned using 10 parameters:
$\lambda_{z}$ $\lambda_{\phi}$ $\lambda_{\theta}$ $\lambda_{\psi}$ $k_{z}$ $k_{\phi}$ $k_{\theta}$ $k_{\psi}$ $k_{p}$ $k_{d}$ $\gamma$
High Values of
High Value of
Too High value of roll
and pitch
oscillations. Also, high
Our tuned parameters are included in Part 5: Performance Testing in Gazebo
Implement a ROS node in Python or MATLAB to evaluate the performance of the
control design on the Crazyflie 2.0 quadrotor in Gazebo. You can create a new ROS package named project
under the project workspace for this purpose. The script must implement the trajectories generated in Part 1 and the sliding mode control laws formulated in Part 2.
Solution:
The drone publishes current pose on /crazyflie2/ground_truth/odometry
topic.
We have created a subscriber that subscribes to this topic and converts it into def odom_callback(self, msg)
function.
Controller 1 is implemented as follows
s1_error_dot = vel_z-zd_dot
s1_error = pos_z-zd
s1 = (s1_error_dot) + self.lamda_z*(s1_error)
u1 = self.m * (self.g + zd_ddot - (self.lamda_z*s1_error_dot) -
(self.k_z*self.saturation(s1)))/(cos(theta)*cos(phi))
First, calculate the
s1_error_dot = vel_z-zd_dot
s1_error = pos_z-zd
s1 = (s1_error_dot) + self.lamda_z*(s1_error)
Then, the controller is implemented as explained in
u1 = self.m * (self.g + zd_ddot - (self.lamda_z*s1_error_dot) -
(self.k_z*self.saturation(s1)))/(cos(theta)*cos(phi))
Controller 2 is implemented as follows
y_error = pos_y - yd
y_error_dot = vel_y - yd_dot
force_y = self.m * ((-self.kp*y_error) +
(-self.kd*y_error_dot) + yd_ddot)
sin_phi_des = -force_y/u1
phi_des = asin(sin_phi_des)
s2_error_dot = dphi
s2_error = self.wrap_to_pi(phi - phi_des)
s2 = (s2_error_dot) + self.lamda_phi*(s2_error)
u2 = - ((dtheta*dpsi*(self.Iy-self.Iz))-(self.Ip*self.ohm*dtheta)
+ (self.lamda_phi*self.Ix*s2_error_dot)+(self.Ix*self.k_phi*self.saturation(s2)))
The drone moves in
y_error = pos_y - yd
y_error_dot = vel_y - yd_dot
force_y = self.m * ((-self.kp*y_error) +
(-self.kd*y_error_dot) + yd_ddot)
sin_phi_des = -force_y/u1
phi_des = asin(sin_phi_des)
Now, calculate the
s2_error_dot = dphi
s2_error = self.wrap_to_pi(phi - phi_des)
s2 = (s2_error_dot) + self.lamda_phi*(s2_error)
The force needed to roll is applied by controller 2 which is implemented as follows and explained in
u2 = - ((dtheta*dpsi*(self.Iy-self.Iz))-(self.Ip*self.ohm*dtheta)
+ (self.lamda_phi*self.Ix*s2_error_dot)+(self.Ix*self.k_phi*self.saturation(s2)))
Controller 3 is implemented as follows
x_error = pos_x - xd
x_error_dot = vel_x - xd_dot
force_x = self.m * ((-self.kp*x_error) +
(-self.kd*x_error_dot) + xd_ddot)
sin_theta_des = force_x/u1
theta_des = asin(sin_theta_des)
s3_error_dot = dtheta
s3_error = self.wrap_to_pi(theta-theta_des)
s3 = (s3_error_dot) + self.lamda_z*(s3_error)
u3 = -((dphi*dpsi*(self.Iz-self.Ix))+(self.Ip*self.ohm*dphi)
+ (self.Iy*self.lamda_theta*s3_error_dot)+(self.Iy*self.k_theta*self.saturation(s3)))
The drone moves in
x_error = pos_x - xd
x_error_dot = vel_x - xd_dot
force_x = self.m * ((-self.kp*x_error) +
(-self.kd*x_error_dot) + xd_ddot)
sin_theta_des = force_x/u1
theta_des = asin(sin_theta_des)
Now, calculate the
s3_error_dot = dtheta
s3_error = self.wrap_to_pi(theta-theta_des)
s3 = (s3_error_dot) + self.lamda_z*(s3_error)
The force needed to pitch is applied by controller 3 which is implemented as follows and explained in
u3 = -((dphi*dpsi*(self.Iz-self.Ix))+(self.Ip*self.ohm*dphi)
+ (self.Iy*self.lamda_theta*s3_error_dot)+(self.Iy*self.k_theta*self.saturation(s3)))
Controller 4 is implemented as follows
s4_error_dot = dpsi
s4_error = self.wrap_to_pi(psi)
s4 = (s4_error_dot) + self.lamda_z*(s4_error)
u4 = -((dphi*dtheta*(self.Ix-self.Iy)) +
(self.lamda_psi * self.Iz*s4_error_dot)+(self.Iz*self.k_psi*self.saturation(s4)))
The drone rotates about its
First, calculate the
s4_error_dot = dpsi
s4_error = self.wrap_to_pi(psi)
s4 = (s4_error_dot) + self.lamda_z*(s4_error)
The force needed to yaw is applied by controller 4 which is implemented as follows and explained in
u4 = -((dphi*dtheta*(self.Ix-self.Iy)) +
(self.lamda_psi * self.Iz*s4_error_dot)+(self.Iz*self.k_psi*self.saturation(s4)))
The Saturation function is used to reduce the chattering on the controller. The saturation function is defined as follows:
def saturation(self, sliding_function):
sat = min(max(sliding_function/self.tol, -1), 1)
return sat
The saturation function works as follows for acceptable error self.tol
:
- When
$s > \gamma$ , saturation function returns 1 - When
$-\gamma < s < \gamma$ , saturation function returns$\frac{s}{\gamma}$ - When
$s < -\gamma$ , saturation function returns -1
Using allocation matrix, as defined in
alloc_mat = np.array((
[
1/(4*self.k_f),
-sqrt(2)/(4*self.k_f*self.l),
-sqrt(2)/(4*self.k_f*self.l),
-1/(4*self.k_m*self.k_f)
],
[
1/(4*self.k_f),
-sqrt(2)/(4*self.k_f*self.l),
sqrt(2)/(4*self.k_f*self.l),
1/(4*self.k_m*self.k_f)
],
[
1/(4*self.k_f),
sqrt(2)/(4*self.k_f*self.l),
sqrt(2)/(4*self.k_f*self.l),
-1/(4*self.k_m*self.k_f)
],
[
1/(4*self.k_f),
sqrt(2)/(4*self.k_f*self.l),
-sqrt(2)/(4*self.k_f*self.l),
1/(4*self.k_m*self.k_f)
]))
self.motor_vel = np.sqrt(np.dot(alloc_mat, u_mat))
It might happen during the control that the required velocity to control the drone is much higher than actually the drone can apply. We need to limit the maximum possible drone velocity. Max possible drone velocity for us is
for i in range(4):
if (self.motor_vel[i] > self.w_max):
self.motor_vel[i] = self.w_max
self.ohm = self.motor_vel[0]-self.motor_vel[1] + self.motor_vel[2]-self.motor_vel[2]
The calculated rotor values are published to topic: /crazyflie2/command/motor_speed
using the self.motor_speed_pub.publish(message)
command.
motor_speed = Actuators()
# note we need to convert in motor velocities
motor_speed.angular_velocities = [self.motor_vel[0], self.motor_vel[1], self.motor_vel[2], self.motor_vel[3]]
self.motor_speed_pub.publish(motor_speed)
Once the program is shut down, the actual trajectory and desired trajectory is saved into a log.pkl
file under the scripts directory. A separate Python script is made to help visualize the trajectory from the saved log.pkl
file. Because we are too lazy to call the script every time we try to run the simulation, we have automated that portion and the system performance plot will automatically pop-up when the simulation is finished.
The desired and actual trajectory is saved into arrays to be plotted. This is done in every iteration using:
self.x_series.append(xyz[0, 0])
self.y_series.append(xyz[1, 0])
self.z_series.append(xyz[2, 0])
self.traj_x_series.append(self.posd[0])
self.traj_y_series.append(self.posd[1])
self.traj_z_series.append(self.posd[2])
We have a command rospy.on_shutdown(self.save_data)
while defining the node which tells the ros node to run the save_data()
function when the ros node shutsdown. The task of this function is to save all the collected data into the log.pkl file. All the arrays created above are dumped into the file using the pickle
library
with open("src/project/scripts/log.pkl", "wb") as fp:
self.mutex_lock_on = True
pickle.dump([self.t_series, self.x_series,
self.y_series, self.z_series, self.traj_x_series, self.traj_y_series, self.traj_z_series], fp)
In order to manually run the visualize.py
file, we use the os
library to run shell commands directly from the python script as shown.
print("Visualizing System Plot")
os.system("rosrun project visualize.py")
visualize.py
has the followng code:
#!/usr/bin/env python3
import matplotlib.pyplot as plt
import pickle
from mpl_toolkits.mplot3d import Axes3D
def visualization(x_series, y_series, z_series, traj_x_series, traj_y_series,traj_z_series):
# load csv file and plot trajectory
fig = plt.figure()
ax = plt.axes(projection='3d')
# Data for a three-dimensional line
ax.plot3D(x_series, y_series, z_series, color='r', label='actual path')
ax.plot3D(traj_x_series, traj_y_series, traj_z_series,
color='b', label='tajectory path')
plt.grid(which='both')
plt.xlabel('x (m)')
plt.ylabel('y (m)')
plt.title("Drone Path")
plt.savefig('src/project/scripts/trajectory.png', dpi=1200)
plt.legend()
plt.show()
if __name__ == '__main__':
file = open("src/project/scripts/log.pkl", 'rb')
t_series, x_series, y_series, z_series, traj_x_series, traj_y_series, traj_z_series = pickle.load(
file)
file.close()
visualization(x_series, y_series, z_series,
traj_x_series, traj_y_series, traj_z_series)
First, we search for log.pkl
file in the directory to extract the time array, actual position arrays and desired positions array.
file = open("src/project/scripts/log.pkl", 'rb')
t_series, x_series, y_series, z_series, traj_x_series, traj_y_series, traj_z_series = pickle.load(
file)
file.close()
Then, the arrays are passed to the visualization(x_series, y_series, z_series, traj_x_series, traj_y_series,traj_z_series)
function to plot them. In this function, we define the nature of the plot like plotting in 3D, color of each plot, labels and the saving the generated performance graph.
The actual trajectory is plotted in color red
and the desired trajectories are plotted in color blue
.
def visualization(x_series, y_series, z_series, traj_x_series, traj_y_series,traj_z_series):
# load csv file and plot trajectory
fig = plt.figure()
ax = plt.axes(projection='3d')
# Data for a three-dimensional line
ax.plot3D(x_series, y_series, z_series, color='r', label='actual path')
ax.plot3D(traj_x_series, traj_y_series, traj_z_series,
color='b', label='tajectory path')
plt.grid(which='both')
plt.xlabel('x (m)')
plt.ylabel('y (m)')
plt.title("Drone Path")
plt.savefig('src/project/scripts/trajectory.png', dpi=1200)
plt.legend()
plt.show()
-
Open a terminal in Ubuntu, and spawn the Crazyflie 2.0 quadrotor on the Gazebo simulator
roslaunch rotors_gazebo crazyflie2_without_controller.launch
Note that the Gazebo environment starts in the paused mode, so make sure that you start the simulation by clicking on the play button before proceed.
-
We can now test the control script developed in Part 3 by running the script in a new terminal. The quadrotor must be controlled smoothly (no overshoot or oscillations) to track the trajectories generated in Part 1 and reach the five desired waypoints.
To launch the rosnode to track the trajectories, type the following command:
rosrun project main.py
-
Tune the performance of the drone to ensure a satisfactory control performance.
Our system is tuned as follows:
$\lambda_{z} = 5$ $\lambda_{\phi} = 10$ $\lambda_{\theta} = 10$ $\lambda_{\psi} = 5$ $k_{z} = 25$ $k_{\phi} = 150$ $k_{\theta} = 150$ $k_{\psi} = 20$ $k_{p} = 50$ $k_{d} = 5$ $\gamma = 0.01$
The performance of our tuning is as shown below. Actual trajectory is plotted in color
red
and the desired trajectories are plotted in colorblue
.
Click the Video below to see the drone simulation:
Our tuning resulted in an acceptable error of 0.01 with $e_{max} = 0.03$ at any possible times.
- Designed for:
- Worcester Polytechnic Institute
- RBE 502: Robot Control - Final Project
- Designed by:
This project is licensed under GNU General Public License v3.0 (see LICENSE.md).
Copyright 2022 Parth Patel Prarthana Sigedar
Licensed under the GNU General Public License, Version 3.0 (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-3.0.en.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.