forked from cesar.alejandro/oscillation_ctrl
Add 'src/klausen_control.py'
This commit is contained in:
parent
4b12a3ea48
commit
bb148364c1
|
@ -0,0 +1,338 @@
|
||||||
|
#!/usr/bin/env python2.7
|
||||||
|
|
||||||
|
### Cesar Rodriguez Aug 2021
|
||||||
|
### Trajectory controller
|
||||||
|
### Based off of Klausen 2017
|
||||||
|
#~~~ Uses generated trajectory and feedback to minimize oscillations
|
||||||
|
#~~~ Controller determines desired forces on drone to dampen oscillations
|
||||||
|
#~~~ which are converted into attitude commands
|
||||||
|
|
||||||
|
import rospy, tf
|
||||||
|
import numpy as np
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
|
||||||
|
from tf.transformations import *
|
||||||
|
from scipy.integrate import odeint
|
||||||
|
from offboard_ex.msg import tethered_status, RefSignal
|
||||||
|
from controller_msgs.msg import FlatTarget
|
||||||
|
from geometry_msgs.msg import Point, Pose
|
||||||
|
from geometry_msgs.msg import TwistStamped, Vector3, Vector3Stamped, PoseStamped, Quaternion
|
||||||
|
from gazebo_msgs.srv import GetLinkState
|
||||||
|
from sensor_msgs.msg import Imu
|
||||||
|
from pymavlink import mavutil
|
||||||
|
|
||||||
|
class Main:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
# rate(s)
|
||||||
|
rate = 200 # rate for the publisher method, specified in Hz -- 20 Hz
|
||||||
|
|
||||||
|
# initialize variables
|
||||||
|
|
||||||
|
# time variables
|
||||||
|
self.dt = 1.0/rate
|
||||||
|
self.tmax = self.dt
|
||||||
|
self.n = self.tmax/self.dt + 1
|
||||||
|
self.t = np.linspace(0, self.tmax, self.n) # Time array
|
||||||
|
|
||||||
|
# Msgs types
|
||||||
|
self.vel_data = TwistStamped() # This is needed to get drone vel from gps
|
||||||
|
self.imu_data = Imu() # Needed for to get drone acc from IMU
|
||||||
|
self.path_pos = np.zeros([3,1])
|
||||||
|
self.path_vel = np.zeros([3,1])
|
||||||
|
self.path_acc = np.zeros([3,1])
|
||||||
|
self.pl_pos = Pose()
|
||||||
|
self.dr_pos = Pose()
|
||||||
|
self.quaternion = PoseStamped()
|
||||||
|
self.EulerAng = [0,0,0] # Will find the euler angles, and then convert to q
|
||||||
|
|
||||||
|
# Drone var
|
||||||
|
self.phi = 0.0 # Payload angle of deflection from x-axis
|
||||||
|
self.phidot = 0.0
|
||||||
|
self.theta = 0.0 # Payload angle of deflection from y-axis
|
||||||
|
self.thetadot = 0.0
|
||||||
|
self.has_run = 0 # Bool to keep track of first run instance
|
||||||
|
self.drone_id = ''
|
||||||
|
self.pload_id = ''
|
||||||
|
# Col1 = theta, theta dot; Col2 = phi, phidot for self.PHI
|
||||||
|
self.PHI = np.array([[0,0],[0,0]])
|
||||||
|
self.dr_vel = np.zeros([3,1])
|
||||||
|
self.dr_angVel = np.zeros([3,1])
|
||||||
|
self.dr_acc = np.zeros([3,1]) #self.imu_data.linear_acceleration
|
||||||
|
|
||||||
|
# Tether var - Check if current method is used
|
||||||
|
# Get tether length
|
||||||
|
self.param_exists = False
|
||||||
|
while self.param_exists == False: # Need to wait until param is ready
|
||||||
|
if rospy.has_param('sim/tether_length'):
|
||||||
|
self.tetherL = rospy.get_param('sim/tether_length') # Tether length
|
||||||
|
self.param_exists = True
|
||||||
|
self.tether = True # Assume we have a tether
|
||||||
|
|
||||||
|
# Tuning gains
|
||||||
|
self.K1 = np.identity(3)
|
||||||
|
self.K2 = np.identity(5)
|
||||||
|
|
||||||
|
# Values which require updates *_p = prior
|
||||||
|
self.z1_p = np.zeros([3,1])
|
||||||
|
self.a45_0 = np.zeros(2)
|
||||||
|
self.alpha = np.zeros([5,1])
|
||||||
|
self.alphadot = np.zeros([5,1])
|
||||||
|
self.a45 = self.alpha[3:5]
|
||||||
|
self.a45dot = np.array([[0],[0]])
|
||||||
|
|
||||||
|
# Error terms
|
||||||
|
self.z1 = np.zeros([3,1]) # dr_pos - ref_sig_pos
|
||||||
|
self.z2 = np.zeros([5,1]) # [vx;vy;vz;thetadot;phidot] - alpha
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
self.drone_m = 1.437
|
||||||
|
self.pl_m = 0.5
|
||||||
|
self.tot_m = self.drone_m + self.pl_m
|
||||||
|
self.tune = 1 # Tuning parameter
|
||||||
|
self.dist = np.array([0,0,0,0.01,0.01]) # Wind disturbance
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
# SUBSCRIBERS
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
# Topic, msg type, and class callback method
|
||||||
|
rospy.Subscriber('/status/twoBody_status', tethered_status, self.linkState_cb)
|
||||||
|
rospy.Subscriber('/reference/path', FlatTarget, self.refsig_cb)
|
||||||
|
rospy.Subscriber('/mavros/local_position/velocity_body', TwistStamped, self.droneVel_cb)
|
||||||
|
rospy.Subscriber('/mavros/imu/data', Imu, self.droneAcc_cb)
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
# PUBLISHERS
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
self.pub_q = rospy.Publisher('/command/quaternions',PoseStamped,queue_size = 10)
|
||||||
|
|
||||||
|
# timer(s), used to control method loop freq(s) as defined by the rate(s)
|
||||||
|
self.pub_time = rospy.Timer(rospy.Duration(1.0/rate), self.publisher) #f this was 5.0 rate before
|
||||||
|
|
||||||
|
# ------------------------------------ _init_ ends ------------------------------ #
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
# CALLBACK FUNCTIONS
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
# Callback to get link names, states, and pload deflection angles
|
||||||
|
def linkState_cb(self,msg):
|
||||||
|
try:
|
||||||
|
self.drone_id = msg.drone_id
|
||||||
|
self.pload_id = msg.pload_id
|
||||||
|
self.dr_pos = msg.drone_pos
|
||||||
|
self.pl_pos = msg.pload_pos
|
||||||
|
#self.tetherL = msg.length
|
||||||
|
self.phi = msg.phi
|
||||||
|
self.phidot = msg.phidot
|
||||||
|
self.theta = msg.theta
|
||||||
|
self.thetadot = msg.thetadot
|
||||||
|
|
||||||
|
# Populate self.PHI
|
||||||
|
self.PHI = np.array([[self.theta,self.thetadot],[self.phi,self.phidot]])
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Callback for drone velocity ####### IS THIS ON? ##########
|
||||||
|
def droneVel_cb(self,msg):
|
||||||
|
try:
|
||||||
|
self.dr_vel = np.array([[msg.twist.linear.x],[msg.twist.linear.y],[msg.twist.linear.z]])
|
||||||
|
self.dr_angVel = np.array([[msg.twist.angular.x],[msg.twist.angular.y],[msg.twist.angular.z]])
|
||||||
|
|
||||||
|
except ValueError or TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Callback for drone accel from IMU data
|
||||||
|
def droneAcc_cb(self,msg):
|
||||||
|
try:
|
||||||
|
self.dr_acc = np.array([[msg.linear_acceleration.x],[msg.linear_acceleration.y],[msg.linear_acceleration.z]])
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Callback reference signal
|
||||||
|
def refsig_cb(self,msg):
|
||||||
|
try:
|
||||||
|
self.path_pos = np.array([[msg.position.x],[msg.position.y],[msg.position.z]])
|
||||||
|
self.path_vel = np.array([[msg.velocity.x],[msg.velocity.y],[msg.velocity.z]])
|
||||||
|
self.path_acc = np.array([[msg.acceleration.x],[msg.acceleration.y],[msg.acceleration.z + 9.81]])
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check if vehicle has tether
|
||||||
|
def tether_check(self):
|
||||||
|
if self.tether == True:
|
||||||
|
rospy.loginfo_once('USING TETHER MODEL')
|
||||||
|
else:
|
||||||
|
rospy.loginfo_once('NO TETHER DETECTED')
|
||||||
|
|
||||||
|
# ---------------------------------ODE SOLVER-------------------------------------#
|
||||||
|
def statespace(self,y,t,Ka,Kb,Kc):
|
||||||
|
# Need the statespace array:
|
||||||
|
a1,a2 = y
|
||||||
|
K = np.dot(Ka,[[a1],[a2]]) + Kb
|
||||||
|
|
||||||
|
# Derivative of statespace array:
|
||||||
|
dydt = np.dot(Kc,K)
|
||||||
|
dydt = [dydt[0][0], dydt[1][0]] # og dydt is list of arrays, need list of floats
|
||||||
|
|
||||||
|
return dydt
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
def control(self):
|
||||||
|
# Populate position vector and gamma (g). g is state space vector: [px,py,pz,theta,phi]
|
||||||
|
p = np.array([[self.dr_pos.position.x],[self.dr_pos.position.y],[self.dr_pos.position.z]])
|
||||||
|
g = np.array([[self.dr_pos.position.x],[self.dr_pos.position.y],[self.dr_pos.position.z],[self.theta],[self.phi]])
|
||||||
|
|
||||||
|
# State some variables for shorthand
|
||||||
|
L = self.tetherL
|
||||||
|
c_theta = math.cos(self.theta)
|
||||||
|
c_phi = math.cos(self.phi)
|
||||||
|
s_theta = math.sin(self.theta)
|
||||||
|
s_phi = math.sin(self.phi)
|
||||||
|
|
||||||
|
# Check for tether
|
||||||
|
if L <= 0.01:
|
||||||
|
self.tether = False
|
||||||
|
else:
|
||||||
|
self.tether = True
|
||||||
|
|
||||||
|
# Check if tether was correctly detected
|
||||||
|
self.tether_check()
|
||||||
|
|
||||||
|
# Control matrices - this may be better in _init_
|
||||||
|
M = [[self.tot_m, 0, 0, 0, L*self.pl_m*c_theta],
|
||||||
|
|
||||||
|
[0, self.tot_m, 0, -L*self.pl_m*c_phi*c_theta, L*self.pl_m*s_phi*s_theta],
|
||||||
|
|
||||||
|
[0, 0, self.tot_m, -L*self.pl_m*c_theta*s_phi, -L*self.pl_m*c_phi*s_theta],
|
||||||
|
|
||||||
|
[0, -L*self.pl_m*c_phi*c_theta,-L*self.pl_m*c_theta*s_phi, (L**2)*self.pl_m*c_theta**2 + 0.01*s_theta**2, 0],
|
||||||
|
|
||||||
|
[L*self.pl_m*c_theta, L*self.pl_m*s_phi*s_theta, -L*self.pl_m*c_phi*s_theta, 0, L**2*self.pl_m]]
|
||||||
|
|
||||||
|
C = [[0,0,0,0,-L*self.thetadot*self.pl_m*s_theta],
|
||||||
|
|
||||||
|
[0,0,0,L*self.pl_m*(self.phidot*c_theta*s_phi + self.thetadot*c_phi*s_theta), L*self.pl_m*(self.phidot*c_phi*s_theta + self.thetadot*c_theta*s_phi)],
|
||||||
|
|
||||||
|
[0,0,0,-L*self.pl_m*(self.phidot*c_phi*c_theta - self.thetadot*s_phi*s_theta),-L*self.pl_m*(self.thetadot*c_phi*c_theta - self.phidot*s_phi*s_theta)],
|
||||||
|
|
||||||
|
[0,0,0,-0.5*(L**2)*self.pl_m*self.thetadot*math.sin(2*self.theta) + 0.5*0.01*self.thetadot*math.sin(2*self.theta), -0.5*(L**2)*self.pl_m*self.phidot*math.sin(2*self.theta)],
|
||||||
|
|
||||||
|
[0,0,0,0.5*(L**2)*self.pl_m*self.phidot*math.sin(2*self.theta),0]]
|
||||||
|
|
||||||
|
G = [[0],[0],[-9.81*self.tot_m],[L*9.81*self.pl_m*c_theta*s_phi],[L*9.81*self.pl_m*c_phi*s_theta]]
|
||||||
|
|
||||||
|
H = [[1,0,0,0,0],[0,1,0,0,0],[0,0,1,0,0]]
|
||||||
|
|
||||||
|
D = np.diag(self.dist) # Already in array format
|
||||||
|
|
||||||
|
# Need to convert to useful format -> np.array
|
||||||
|
M = np.array(M)
|
||||||
|
C = np.array(C)
|
||||||
|
G = np.array(G)
|
||||||
|
H = np.array(H)
|
||||||
|
|
||||||
|
# Shorthand for matrices which will be used
|
||||||
|
M_a = M[3:5,3:5] # M_alpha - rows 4 to 5 and columns 4 to 5 ... looks wrong its not
|
||||||
|
C_a = C[3:5,3:5]
|
||||||
|
D_a = D[3:5,3:5]
|
||||||
|
G_a = G[3:5]
|
||||||
|
M_b = M[3:5,:3] # M_4:5,1:3 - rows 4 to 5 and columns 1 to 3
|
||||||
|
M_c = M[:3,3:5] # M_1:3,4:5 - rows 1 to 3 and columns 4 to 5
|
||||||
|
C_c = C[:3,3:5]
|
||||||
|
|
||||||
|
Ka = -(D_a + C_a + self.K2[3:5,3:5])
|
||||||
|
Kb = -G_a + np.dot(self.K2[3:5,3:5],self.PHI[:,1]) - np.dot(M_b,self.path_acc - np.dot(self.K1,self.dr_vel - self.path_vel))
|
||||||
|
|
||||||
|
# Determine alpha
|
||||||
|
if self.tether:
|
||||||
|
M_aI = np.linalg.inv(M_a) # Inverse of M_a
|
||||||
|
|
||||||
|
# SOLVE ODE (get alpha)
|
||||||
|
# Populate buffer
|
||||||
|
self.a45_buff = odeint(self.statespace,self.a45_0,self.t,args=(Ka,Kb,M_aI))
|
||||||
|
|
||||||
|
# Update a45_0 to new a45. Need to transpose to column vector
|
||||||
|
self.a45_0 = self.a45_buff[1,:]
|
||||||
|
self.a45 = np.array([[self.a45_0[0]],[self.a45_0[1]]])
|
||||||
|
|
||||||
|
# Get alphadot_4:5
|
||||||
|
self.a45dot = self.statespace(self.a45_0,1,Ka,Kb,M_aI) # Do not need 't' and that's why it is a 1
|
||||||
|
self.a45dot = np.array([[self.a45dot[0]],[self.a45dot[1]]])
|
||||||
|
|
||||||
|
else: # if no tether, alpha_4:5 = 0
|
||||||
|
self.a45 = np.array([[0],[0]])
|
||||||
|
self.a45dot = np.array([[0],[0]])
|
||||||
|
|
||||||
|
# Determine a_1:3
|
||||||
|
self.alpha[:3] = self.path_vel - np.dot(self.K1,p - self.path_pos)
|
||||||
|
|
||||||
|
# populate error terms
|
||||||
|
self.z1 = p - self.path_pos
|
||||||
|
z1_dot = self.dr_vel - self.path_vel
|
||||||
|
self.z2 = g - self.alpha
|
||||||
|
|
||||||
|
B = np.dot(C_c,self.a45) + np.dot(M_c,self.a45dot)
|
||||||
|
|
||||||
|
# Gain terms
|
||||||
|
Kp = np.identity(3) + np.dot(self.K2[:3,:3],self.K1) + self.tune*np.identity(3)
|
||||||
|
Kd = self.tot_m*self.K1 + self.K2[:3,:3]
|
||||||
|
Ki = self.tune*self.K1
|
||||||
|
|
||||||
|
# Desired body-oriented forces
|
||||||
|
Fd = B + G[:3] + self.tot_m*self.dr_acc - np.dot(Kd,z1_dot) - np.dot(Kp,self.z1) - np.dot(Ki,self.dt*(self.z1 - self.z1_p))
|
||||||
|
|
||||||
|
# Update self.z1_p for "integration"
|
||||||
|
self.z1_p = self.z1
|
||||||
|
|
||||||
|
# Covert Fd into drone frame
|
||||||
|
dr_orientation = [self.dr_pos.orientation.x, self.dr_pos.orientation.y, self.dr_pos.orientation.z, self.dr_pos.orientation.w]
|
||||||
|
dr_orientation_inv = quaternion_inverse(dr_orientation)
|
||||||
|
|
||||||
|
Fd_tf = quaternion_multiply(dr_orientation,quaternion_multiply([Fd[0],Fd[1],Fd[2],0],dr_orientation_inv)) # Real part of Fd needs = 0
|
||||||
|
|
||||||
|
# Convert forces to attiude *EulerAng[2] = yaw = 0
|
||||||
|
self.EulerAng[1] = math.atan(-Fd_tf[0]/(self.drone_m*9.81)) # Pitch -- maybe
|
||||||
|
self.EulerAng[0] = math.atan(Fd_tf[1]*math.cos(self.EulerAng[1])/(self.drone_m*9.81)) # Roll -- maybe
|
||||||
|
|
||||||
|
# Convert Euler angles to quaternions
|
||||||
|
q = quaternion_from_euler(self.EulerAng[0],self.EulerAng[1],self.EulerAng[2])
|
||||||
|
|
||||||
|
# Populate msg variable
|
||||||
|
|
||||||
|
# Attitude control
|
||||||
|
self.quaternion.header.stamp = rospy.Time.now()
|
||||||
|
self.quaternion.pose.position.x = 0
|
||||||
|
self.quaternion.pose.position.y = 0
|
||||||
|
self.quaternion.pose.position.z = 5.0
|
||||||
|
self.quaternion.pose.orientation.x = q[0]
|
||||||
|
self.quaternion.pose.orientation.y = q[1]
|
||||||
|
self.quaternion.pose.orientation.z = q[2]
|
||||||
|
self.quaternion.pose.orientation.w = q[3]
|
||||||
|
|
||||||
|
def publisher(self,pub_time):
|
||||||
|
self.control()
|
||||||
|
self.pub_q.publish(self.quaternion)
|
||||||
|
# rospy.loginfo('roll: %.2f\npitch: %.2f\n',self.EulerAng[0],self.EulerAng[1])
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
# ALGORITHM
|
||||||
|
# --------------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
|
||||||
|
# Initiate ROS node
|
||||||
|
rospy.init_node('trajectoryCtrl',anonymous=True)
|
||||||
|
try:
|
||||||
|
Main() # create class object
|
||||||
|
rospy.spin() # loop until shutdown signal
|
||||||
|
|
||||||
|
except rospy.ROSInterruptException:
|
||||||
|
pass
|
Loading…
Reference in New Issue