auv_geometric_tracking_controller.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # Copyright (c) 2016-2019 The UUV Simulator Authors.
3 # All rights reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 import math
17 import numpy as np
18 import rospy
19 from copy import deepcopy
20 
21 import geometry_msgs.msg as geometry_msgs
22 import uuv_control_msgs.msg as uuv_control_msgs
23 import tf
24 import tf.transformations as trans
25 
26 from nav_msgs.msg import Odometry
27 from uuv_thrusters.models import Thruster
28 from uuv_gazebo_ros_plugins_msgs.msg import FloatStamped
29 from uuv_control_msgs.msg import TrajectoryPoint
30 from uuv_control_interfaces import DPControllerLocalPlanner
31 import tf2_ros
32 from tf_quaternion.transformations import quaternion_matrix
33 
34 
36  def __init__(self):
37  self.namespace = rospy.get_namespace().replace('/', '')
38  rospy.loginfo('Initialize control for vehicle <%s>' % self.namespace)
39 
40  self.local_planner = DPControllerLocalPlanner(full_dof=True, thrusters_only=False,
41  stamped_pose_only=False)
42 
43  self.base_link = rospy.get_param('~base_link', 'base_link')
44 
45  # Reading the minimum thrust generated
46  self.min_thrust = rospy.get_param('~min_thrust', 0)
47  assert self.min_thrust >= 0
48  rospy.loginfo('Min. thrust [N]=%.2f', self.min_thrust)
49 
50  # Reading the maximum thrust generated
51  self.max_thrust = rospy.get_param('~max_thrust', 0)
52  assert self.max_thrust > 0 and self.max_thrust > self.min_thrust
53  rospy.loginfo('Max. thrust [N]=%.2f', self.max_thrust)
54 
55  # Reading the thruster topic
56  self.thruster_topic = rospy.get_param('~thruster_topic', 'thrusters/0/input')
57  assert len(self.thruster_topic) > 0
58 
59  # Reading the thruster gain
60  self.p_gain_thrust = rospy.get_param('~thrust_p_gain', 0.0)
61  assert self.p_gain_thrust > 0
62 
63  self.d_gain_thrust = rospy.get_param('~thrust_d_gain', 0.0)
64  assert self.d_gain_thrust >= 0
65 
66  # Reading the roll gain
67  self.p_roll = rospy.get_param('~p_roll', 0.0)
68  assert self.p_roll > 0
69 
70  # Reading the pitch P gain
71  self.p_pitch = rospy.get_param('~p_pitch', 0.0)
72  assert self.p_pitch > 0
73 
74  # Reading the pitch D gain
75  self.d_pitch = rospy.get_param('~d_pitch', 0.0)
76  assert self.d_pitch >= 0
77 
78  # Reading the yaw P gain
79  self.p_yaw = rospy.get_param('~p_yaw', 0.0)
80  assert self.p_yaw > 0
81 
82  # Reading the yaw D gain
83  self.d_yaw = rospy.get_param('~d_yaw', 0.0)
84  assert self.d_yaw >= 0
85 
86  # Reading the saturation for the desired pitch
87  self.desired_pitch_limit = rospy.get_param('~desired_pitch_limit', 15 * np.pi / 180)
88  assert self.desired_pitch_limit > 0
89 
90  # Reading the saturation for yaw error
91  self.yaw_error_limit = rospy.get_param('~yaw_error_limit', 1.0)
92  assert self.yaw_error_limit > 0
93 
94  # Reading the number of fins
95  self.n_fins = rospy.get_param('~n_fins', 0)
96  assert self.n_fins > 0
97 
98  # Reading the mapping for roll commands
99  self.map_roll = rospy.get_param('~map_roll', [0, 0, 0, 0])
100  assert isinstance(self.map_roll, list)
101  assert len(self.map_roll) == self.n_fins
102 
103  # Reading the mapping for the pitch commands
104  self.map_pitch = rospy.get_param('~map_pitch', [0, 0, 0, 0])
105  assert isinstance(self.map_pitch, list)
106  assert len(self.map_pitch) == self.n_fins
107 
108  # Reading the mapping for the yaw commands
109  self.map_yaw = rospy.get_param('~map_yaw', [0, 0, 0, 0])
110  assert isinstance(self.map_yaw, list)
111  assert len(self.map_yaw) == self.n_fins
112 
113  # Retrieve the thruster configuration parameters
114  self.thruster_config = rospy.get_param('~thruster_config')
115 
116  # Check if all necessary thruster model parameter are available
117  thruster_params = ['conversion_fcn_params', 'conversion_fcn',
118  'topic_prefix', 'topic_suffix', 'frame_base', 'max_thrust']
119  for p in thruster_params:
120  if p not in self.thruster_config:
121  raise rospy.ROSException(
122  'Parameter <%s> for thruster conversion function is '
123  'missing' % p)
124 
125  # Setting up the thruster topic name
126  self.thruster_topic = '/%s/%s/%d/%s' % (self.namespace,
127  self.thruster_config['topic_prefix'], 0,
128  self.thruster_config['topic_suffix'])
129 
130  base = '%s/%s' % (self.namespace, self.base_link)
131 
132  frame = '%s/%s%d' % (self.namespace, self.thruster_config['frame_base'], 0)
133 
136 
137  rospy.loginfo('Lookup: Thruster transform found %s -> %s' % (base, frame))
138  trans = self.tf_buffer.lookup_transform(base, frame, rospy.Time(), rospy.Duration(5))
139  pos = np.array([trans.transform.translation.x,
140  trans.transform.translation.y,
141  trans.transform.translation.z])
142  quat = np.array([trans.transform.rotation.x,
143  trans.transform.rotation.y,
144  trans.transform.rotation.z,
145  trans.transform.rotation.w])
146  rospy.loginfo('Thruster transform found %s -> %s' % (base, frame))
147  rospy.loginfo('pos=' + str(pos))
148  rospy.loginfo('rot=' + str(quat))
149 
150  # Read transformation from thruster
151  self.thruster = Thruster.create_thruster(
152  self.thruster_config['conversion_fcn'], 0,
153  self.thruster_topic, pos, quat,
154  **self.thruster_config['conversion_fcn_params'])
155 
156  rospy.loginfo('Thruster configuration=\n' + str(self.thruster_config))
157  rospy.loginfo('Thruster input topic=' + self.thruster_topic)
158 
159  self.max_fin_angle = rospy.get_param('~max_fin_angle', 0.0)
160  assert self.max_fin_angle > 0
161 
162  # Reading the fin input topic prefix
163  self.fin_topic_prefix = rospy.get_param('~fin_topic_prefix', 'fins')
164  self.fin_topic_suffix = rospy.get_param('~fin_topic_suffix', 'input')
165 
166  self.rpy_to_fins = np.vstack((self.map_roll, self.map_pitch, self.map_yaw)).T
167 
168  self.pub_cmd = list()
169 
170  for i in range(self.n_fins):
171  topic = '%s/%d/%s' % (self.fin_topic_prefix, i, self.fin_topic_suffix)
172  self.pub_cmd.append(
173  rospy.Publisher(topic, FloatStamped, queue_size=10))
174 
175  self.odometry_sub = rospy.Subscriber(
176  'odom', Odometry, self.odometry_callback)
177 
178  self.reference_pub = rospy.Publisher(
179  'reference', TrajectoryPoint, queue_size=1)
180 
181  # Publish error (for debugging)
182  self.error_pub = rospy.Publisher(
183  'error', TrajectoryPoint, queue_size=1)
184 
185  @staticmethod
186  def unwrap_angle(t):
187  return math.atan2(math.sin(t),math.cos(t))
188 
189  @staticmethod
190  def vector_to_np(v):
191  return np.array([v.x, v.y, v.z])
192 
193  @staticmethod
195  return np.array([q.x, q.y, q.z, q.w])
196 
197  def odometry_callback(self, msg):
198  """Handle odometry callback: The actual control loop."""
199 
200  # Update local planner's vehicle position and orientation
201  pos = [msg.pose.pose.position.x,
202  msg.pose.pose.position.y,
203  msg.pose.pose.position.z]
204 
205  quat = [msg.pose.pose.orientation.x,
206  msg.pose.pose.orientation.y,
207  msg.pose.pose.orientation.z,
208  msg.pose.pose.orientation.w]
209 
210  self.local_planner.update_vehicle_pose(pos, quat)
211 
212  # Compute the desired position
213  t = rospy.Time.now().to_sec()
214  des = self.local_planner.interpolate(t)
215 
216  # Publish the reference
217  ref_msg = TrajectoryPoint()
218  ref_msg.header.stamp = rospy.Time.now()
219  ref_msg.header.frame_id = self.local_planner.inertial_frame_id
220  ref_msg.pose.position = geometry_msgs.Vector3(*des.p)
221  ref_msg.pose.orientation = geometry_msgs.Quaternion(*des.q)
222  ref_msg.velocity.linear = geometry_msgs.Vector3(*des.vel[0:3])
223  ref_msg.velocity.angular = geometry_msgs.Vector3(*des.vel[3::])
224 
225  self.reference_pub.publish(ref_msg)
226 
227  p = self.vector_to_np(msg.pose.pose.position)
228  forward_vel = self.vector_to_np(msg.twist.twist.linear)
229  ref_vel = des.vel[0:3]
230 
231  q = self.quaternion_to_np(msg.pose.pose.orientation)
232  rpy = trans.euler_from_quaternion(q, axes='sxyz')
233 
234  # Compute tracking errors wrt world frame:
235  e_p = des.p - p
236  abs_pos_error = np.linalg.norm(e_p)
237  abs_vel_error = np.linalg.norm(ref_vel - forward_vel)
238 
239  # Generate error message
240  error_msg = TrajectoryPoint()
241  error_msg.header.stamp = rospy.Time.now()
242  error_msg.header.frame_id = self.local_planner.inertial_frame_id
243  error_msg.pose.position = geometry_msgs.Vector3(*e_p)
244  error_msg.pose.orientation = geometry_msgs.Quaternion(
245  *trans.quaternion_multiply(trans.quaternion_inverse(q), des.q))
246  error_msg.velocity.linear = geometry_msgs.Vector3(*(des.vel[0:3] - self.vector_to_np(msg.twist.twist.linear)))
247  error_msg.velocity.angular = geometry_msgs.Vector3(*(des.vel[3::] - self.vector_to_np(msg.twist.twist.angular)))
248 
249  # Based on position tracking error: Compute desired orientation
250  pitch_des = -math.atan2(e_p[2], np.linalg.norm(e_p[0:2]))
251  # Limit desired pitch angle:
252  pitch_des = max(-self.desired_pitch_limit, min(pitch_des, self.desired_pitch_limit))
253 
254  yaw_des = math.atan2(e_p[1], e_p[0])
255  yaw_err = self.unwrap_angle(yaw_des - rpy[2])
256 
257  # Limit yaw effort
258  yaw_err = min(self.yaw_error_limit, max(-self.yaw_error_limit, yaw_err))
259 
260  # Roll: P controller to keep roll == 0
261  roll_control = self.p_roll * rpy[0]
262 
263  # Pitch: P controller to reach desired pitch angle
264  pitch_control = self.p_pitch * self.unwrap_angle(pitch_des - rpy[1]) + self.d_pitch * (des.vel[4] - msg.twist.twist.angular.y)
265 
266  # Yaw: P controller to reach desired yaw angle
267  yaw_control = self.p_yaw * yaw_err + self.d_yaw * (des.vel[5] - msg.twist.twist.angular.z)
268 
269  # Limit thrust
270  thrust = min(self.max_thrust, self.p_gain_thrust * np.linalg.norm(abs_pos_error) + self.d_gain_thrust * abs_vel_error)
271  thrust = max(self.min_thrust, thrust)
272 
273  rpy = np.array([roll_control, pitch_control, yaw_control])
274 
275  # In case the world_ned reference frame is used, the convert it back
276  # to the ENU convention to generate the reference fin angles
277  rtf = deepcopy(self.rpy_to_fins)
278  if self.local_planner.inertial_frame_id == 'world_ned':
279  rtf[:, 1] *= -1
280  rtf[:, 2] *= -1
281  # Transform orientation command into fin angle set points
282  fins = rtf.dot(rpy)
283 
284  # Check for saturation
285  max_angle = max(np.abs(fins))
286  if max_angle >= self.max_fin_angle:
287  fins = fins * self.max_fin_angle / max_angle
288 
289  thrust_force = self.thruster.tam_column * thrust
290  self.thruster.publish_command(thrust_force[0])
291 
292  cmd = FloatStamped()
293  for i in range(self.n_fins):
294  cmd.header.stamp = rospy.Time.now()
295  cmd.header.frame_id = '%s/fin%d' % (self.namespace, i)
296  cmd.data = min(fins[i], self.max_fin_angle)
297  cmd.data = max(cmd.data, -self.max_fin_angle)
298  self.pub_cmd[i].publish(cmd)
299 
300  self.error_pub.publish(error_msg)
301 
302 
303 if __name__ == '__main__':
304  print('Starting AUV trajectory tracker')
305  rospy.init_node('auv_geometric_tracking_controller')
306 
307  try:
309  rospy.spin()
310  except rospy.ROSInterruptException:
311  print('caught exception')


uuv_trajectory_control
Author(s):
autogenerated on Thu Jun 18 2020 03:28:42