33
44import rclpy
55from rclpy .node import Node
6+ import tf2_ros
67
78from sensor_msgs .msg import JointState
8- from geometry_msgs .msg import Twist
9+ from geometry_msgs .msg import Twist , TwistWithCovariance , TransformStamped
10+ from nav_msgs .msg import Odometry
11+ from std_msgs .msg import Float64
912from osr_interfaces .msg import CommandDrive , CommandCorner
1013
1114
@@ -24,7 +27,8 @@ def __init__(self):
2427 ('rover_dimensions.d3' , None ),
2528 ('rover_dimensions.d4' , None ),
2629 ('rover_dimensions.wheel_radius' , None ),
27- ('drive_no_load_rpm' , None )
30+ ('drive_no_load_rpm' , None ),
31+ ('enable_odometry' , None )
2832 ]
2933 )
3034 self .d1 = self .get_parameter ('rover_dimensions.d1' ).get_parameter_value ().double_value
@@ -41,7 +45,16 @@ def __init__(self):
4145 drive_no_load_rpm = self .get_parameter (
4246 "drive_no_load_rpm" ).get_parameter_value ().double_value
4347 self .max_vel = self .wheel_radius * drive_no_load_rpm / 60 * 2 * math .pi # [m/s]
44- self .curr_twist = Twist ()
48+ self .should_calculate_odom = self .get_parameter ("enable_odometry" ).get_parameter_value ().bool_value
49+ if self .should_calculate_odom :
50+ self .get_logger ().info ("Calculting wheel odometry and publishing to /odom topic" )
51+ self .odometry = Odometry ()
52+ self .odometry .header .stamp = self .get_clock ().now ().to_msg ()
53+ self .odometry .header .frame_id = "odom"
54+ self .odometry .child_frame_id = "base_link"
55+ self .odometry .pose .pose .orientation .z = 1.
56+ self .odometry .pose .pose .orientation .w = 1.
57+ self .curr_twist = TwistWithCovariance ()
4558 self .curr_turning_radius = self .max_radius
4659
4760 self .cmd_vel_sub = self .create_subscription (Twist , "/cmd_vel" ,
@@ -50,6 +63,11 @@ def __init__(self):
5063 partial (self .cmd_cb , intuitive = True ), 1 )
5164 self .encoder_sub = self .create_subscription (JointState , "/encoder" , self .enc_cb , 1 )
5265
66+ self .turning_radius_pub = self .create_publisher (Float64 , "/turning_radius" , 1 )
67+ if self .should_calculate_odom :
68+ self .odometry_pub = self .create_publisher (Odometry , "/odom" , 2 )
69+ self .tf_pub = tf2_ros .TransformBroadcaster (self )
70+
5371 self .corner_cmd_pub = self .create_publisher (CommandCorner , "/cmd_corner" , 1 )
5472 self .drive_cmd_pub = self .create_publisher (CommandDrive , "/cmd_drive" , 1 )
5573
@@ -94,7 +112,40 @@ def cmd_cb(self, twist_msg, intuitive=False):
94112 def enc_cb (self , msg ):
95113 self .curr_positions = dict (zip (msg .name , msg .position ))
96114 self .curr_velocities = dict (zip (msg .name , msg .velocity ))
97- self .forward_kinematics ()
115+ if self .should_calculate_odom :
116+ # measure how much time has elapsed since our last update
117+ now = self .get_clock ().now ()
118+ dt = float (now .nanoseconds - (self .odometry .header .stamp .sec * 10 ** 9 + self .odometry .header .stamp .nanosec )) / 10 ** 9
119+ self .forward_kinematics ()
120+ dx = self .curr_twist .twist .linear .x * dt
121+ dth = self .curr_twist .twist .angular .z * dt
122+ # angle is straightforward: in 2D it's additive
123+ # first calculate the current_angle in the fixed frame
124+ current_angle = 2 * math .atan2 (self .odometry .pose .pose .orientation .z ,
125+ self .odometry .pose .pose .orientation .w )
126+ new_angle = current_angle + dth
127+ self .odometry .pose .pose .orientation .z = math .sin (new_angle / 2. )
128+ self .odometry .pose .pose .orientation .w = math .cos (new_angle / 2. )
129+ # the new pose in x and y depends on the current heading
130+ self .odometry .pose .pose .position .x += math .cos (new_angle ) * dx
131+ self .odometry .pose .pose .position .y += math .sin (new_angle ) * dx
132+ self .odometry .pose .covariance = 36 * [0.0 ,]
133+ # explanation for values at https://www.freedomrobotics.ai/blog/tuning-odometry-for-wheeled-robots
134+ self .odometry .pose .covariance [0 ] = 0.0225
135+ self .odometry .pose .covariance [5 ] = 0.01
136+ self .odometry .pose .covariance [- 5 ] = 0.0225
137+ self .odometry .pose .covariance [- 1 ] = 0.04
138+ self .odometry .twist = self .curr_twist
139+ self .odometry .header .stamp = now .to_msg ()
140+ self .odometry_pub .publish (self .odometry )
141+ transform_msg = TransformStamped ()
142+ transform_msg .header .frame_id = "odom"
143+ transform_msg .child_frame_id = "base_link"
144+ transform_msg .header .stamp = now .to_msg ()
145+ transform_msg .transform .translation .x = self .odometry .pose .pose .position .x
146+ transform_msg .transform .translation .y = self .odometry .pose .pose .position .y
147+ transform_msg .transform .rotation = self .odometry .pose .pose .orientation
148+ self .tf_pub .sendTransform (transform_msg )
98149
99150 def corner_cmd_threshold (self , corner_cmd ):
100151 try :
@@ -292,6 +343,18 @@ def forward_kinematics(self):
292343 self .get_logger ().debug ("Current approximate turning radius: {}" .format (round (approx_turning_radius , 2 )))
293344 self .curr_turning_radius = approx_turning_radius
294345
346+ # we know that the linear velocity in x direction is the instantaneous velocity of the middle virtual
347+ # wheel which spins at the average speed of the two middle outer wheels.
348+ drive_angular_velocity = (self .curr_velocities ['drive_left_middle' ] + self .curr_velocities ['drive_right_middle' ]) / 2.
349+ self .curr_twist .twist .linear .x = drive_angular_velocity * self .wheel_radius
350+ # now calculate angular velocity from its relation with linear velocity and turning radius
351+ try :
352+ self .curr_twist .twist .angular .z = self .curr_twist .twist .linear .x / self .curr_turning_radius
353+ except ZeroDivisionError :
354+ self .get_logger ().warn ("Current turning radius was calculated as zero which"
355+ "is an illegal value. Check your wheel calibration." )
356+ self .curr_twist .twist .angular .z = 0. # turning in place is currently unsupported
357+
295358
296359def main (args = None ):
297360 rclpy .init (args = args )
0 commit comments