Roadmap
Controllers
Differential drive controller
The most popular and easiest to implement controller. Transforms angular velocity w and linear velocity v to wheel velocities vl and vr . Ideal for small robots and robot vacuums.
It already has implementation in ROS1 and Gazebo, you just need to add libdiffdrive_plugin.so
, set distance between wheels, wheel radius, what URDF joints to use, etc. Then send v and w through cmd_vel
topic and that’s pretty much all.
But….. Our car model has six continuous joints instead of two:
left steering hinge joint
right steering hinge joint
front left wheel joint
front right wheel joint
rear left wheel joint
rear right wheel joint
Ackermann steering drive controller
This controller is good for bicycles and cars. It should transform speed s and steering angle of the virtual center wheel φ , like on a tricycle, to two steering and two wheel commands.
ROS1 has an implementation of this controller, but it:
uses cmd_vel
topic with Twist
message instead of AckermannDrive
doesn’t work good with more than two wheels
So we will implement our version of this controller. A good tutorial shows us how simulating a robot’s controllers in Gazebo can be accomplished using ros_control and a simple Gazebo plugin adapter.
Add car control step by step with ros_controls
Add transmission elements to a URDF as macros. The most important part is hardwareInterface. Read more about transmission and hardware interfaces .
<!-- deep_ws/src/deepracer_car/urdf/xacro/macro/macros.xacro -->
<xacro:macro name= "wheel_transmission" params= "name" >
<transmission name= "${name}_transmission" type= "SimpleTransmission" >
<type> transmission_interface/SimpleTransmission</type>
<joint name= "${name}_joint" >
<hardwareInterface> hardware_interface/VelocityJointInterface</hardwareInterface>
</joint>
<actuator name= "${name}_motor" >
<hardwareInterface> hardware_interface/VelocityJointInterface</hardwareInterface>
<mechanicalReduction> 1</mechanicalReduction>
</actuator>
</transmission>
</xacro:macro>
<xacro:macro name= "steering_hinge_transmission" params= "name" >
<transmission name= "${name}_transmission" type= "SimpleTransmission" >
<type> transmission_interface/SimpleTransmission</type>
<joint name= "${name}_joint" >
<hardwareInterface> hardware_interface/PositionJointInterface</hardwareInterface>
</joint>
<actuator name= "${name}_motor" >
<hardwareInterface> hardware_interface/PositionJointInterface</hardwareInterface>
<mechanicalReduction> 1</mechanicalReduction>
</actuator>
</transmission>
</xacro:macro>
Add the gazebo_ros_control plugin that actually parses the transmission tags and loads the appropriate hardware interfaces and controller manager
<!-- deep_ws/src/deepracer_car/urdf/xacro/control/deepracer_ros_control.xacro -->
<gazebo>
<plugin name= "gazebo_ros_control" filename= "libgazebo_ros_control.so" >
<robotNamespace> /</robotNamespace>
<robotSimType> gazebo_ros_control/DefaultRobotHWSim</robotSimType>
<legacyModeNS> true</legacyModeNS>
</plugin>
</gazebo>
Create a deep_ws/src/deepracer_car/config/racecar_control.yaml
config file
# Publish all joint states -----------------------------------
joint_state_controller :
type : joint_state_controller/JointStateController
publish_rate : 60
# add four wheel controllers
left_rear_wheel_velocity_controller :
type : velocity_controllers/JointVelocityController
joint : left_rear_wheel_joint
pid : { p : 1.0 , i : 0.0 , d : 0.0 , i_clamp : 0.0 }
# add two steering controllers
right_steering_hinge_position_controller :
type : position_controllers/JointPositionController
joint : right_steering_hinge_joint
pid : { p : 1.0 , i : 0.0 , d : 0.5 }
Also add PID gains for gazebo_ros_control in that same config file
gazebo_ros_control/pid_gains :
left_rear_wheel_joint : { p : 0.1 , i : 0.0 , d : 0.0 , i_clamp : 0.0 }
right_rear_wheel_joint : { p : 0.1 , i : 0.0 , d : 0.0 , i_clamp : 0.0 }
left_front_wheel_joint : { p : 0.1 , i : 0.0 , d : 0.0 , i_clamp : 0.0 }
right_front_wheel_joint : { p : 0.1 , i : 0.0 , d : 0.0 , i_clamp : 0.0 }
left_steering_hinge_joint : { p : 0.05 , i : 0.0 , d : 0.0 }
right_steering_hinge_joint : { p : 0.05 , i : 0.0 , d : 0.0 }
And finally create a racecar_control.launch
file
<!-- deep_ws/src/deepracer_car/launch/racecar_control.launch -->
<?xml version='1.0'?>
<launch>
<!-- Load joint controller configurations from YAML file to parameter server -->
<rosparam file= "$(find deepracer_car)/config/racecar_control.yaml" command= "load" />
<!-- convert joint states to TF transforms for rviz, etc -->
<node name= "robot_state_publisher" pkg= "robot_state_publisher" type= "robot_state_publisher" output= "screen" />
<!-- The controller_spawner node starts controllers by running a python script that makes a service call to the ros_control controller manager. The service calls tell the controller manager which controllers you want. It also loads a controller that publishes the joint states of all the joints with hardware_interfaces and advertises the topic on /joint_states. The spawner is just a helper script for use with roslaunch. -->
<node name= "controller_manager" pkg= "controller_manager" type= "spawner" respawn= "false"
output= "screen" args= "left_rear_wheel_velocity_controller
right_rear_wheel_velocity_controller
left_front_wheel_velocity_controller
right_front_wheel_velocity_controller
left_steering_hinge_position_controller
right_steering_hinge_position_controller
joint_state_controller" />
</launch>
Fantastic. Now include this file to racecar.launch
<!-- deep_ws/src/deepracer_car/launch/racecar.launch -->
<include file= "$(find deepracer_car)/launch/racecar_control.launch" />
and we can start simulation with control
roslaunch simulation simulation.launch
and even send commands to each joint
rostopic pub -1 /left_rear_wheel_velocity_controller/command std_msgs/Float64 "data: 1.5"
So far, so good. But do we really need to send 6 commands to control a car?
Control node
Lets add a node for more convinient control. Create a control_deepracer_car.py
in a deep_ws/src/deepracer_car/scripts
folder. Make a CarController
with six command publishers and one AckermannDriveStamped
subscriber
#!/usr/bin/env python3
import rospy
from ackermann_msgs.msg import AckermannDriveStamped
from std_msgs.msg import Float64MultiArray , Float64
from collections import OrderedDict
import math
import threading
class CarController ():
def __init__ ( self ):
self . max_speed = rospy . get_param ( "~max_speed" , 4.0 )
self . max_steering_angle = rospy . get_param ( "~max_steering_angle" , 0.523599 )
self . _wheel_radius = rospy . get_param ( "~wheel_radius" , 0.03 )
self . _wheel_separation = rospy . get_param ( "~wheel_separation" , 0.159202 )
self . _wheel_base = rospy . get_param ( "~wheel_base" , 0.164023 )
self . update_rate = rospy . get_param ( "~update_rate" , 50 ) # Hz
self . _cmd_lock = threading . Lock ()
# Car speed (m/s)
self . speed = 0
# Steering angle (rad)
self . steering_angle = 0
# Zero steering angle velocity means change
# the steering angle as quickly as possible.
self . steering_angle_velocity = 0
# Create publishers for controlling the car
self . _velocity_pub_dict_ = OrderedDict ()
self . _steering_pub_dict_ = OrderedDict ()
# 4 wheel publishers
self . _velocity_pub_dict_ [ "l_rear_wheel" ] = rospy . Publisher ( '/left_rear_wheel_velocity_controller/command' , Float64 , queue_size = 1 )
self . _velocity_pub_dict_ [ "r_rear_wheel" ] = rospy . Publisher ( '/right_rear_wheel_velocity_controller/command' , Float64 , queue_size = 1 )
self . _velocity_pub_dict_ [ "l_front_wheel" ] = rospy . Publisher ( '/left_front_wheel_velocity_controller/command' , Float64 , queue_size = 1 )
self . _velocity_pub_dict_ [ "r_front_wheel" ] = rospy . Publisher ( '/right_front_wheel_velocity_controller/command' , Float64 , queue_size = 1 )
# 2 steering publishers
self . _steering_pub_dict_ [ 'left' ] = rospy . Publisher ( '/left_steering_hinge_position_controller/command' , Float64 , queue_size = 1 )
self . _steering_pub_dict_ [ 'right' ] = rospy . Publisher ( '/right_steering_hinge_position_controller/command' , Float64 , queue_size = 1 )
self . cmd_sub = rospy . Subscriber ( "/ackermann_cmd" , AckermannDriveStamped ,
self . ackermann_cmd_cb , queue_size = 1 )
self . control ()
if __name__ == '__main__' :
try :
rospy . init_node ( 'control_deepracer_car' , anonymous = True , log_level = rospy . INFO )
node = CarController ()
except KeyboardInterrupt :
print ( "Shutting down ROS control_deepracer_car node" )
Subscriber will update current speed and angle from ackermann_cmd
topic
def ackermann_cmd_cb ( self , msg ):
with self . _cmd_lock :
self . speed = msg . drive . speed
self . steering_angle = msg . drive . steering_angle
self . steering_angle_velocity = msg . drive . steering_angle_velocity
Control will spin the node with a set rate
def control ( self ):
rate = rospy . Rate ( self . update_rate )
update_period = 1 / self . update_rate
last_time = rospy . get_time ()
while not rospy . is_shutdown ():
t = rospy . get_time ()
delta_t = t - last_time
if delta_t >= update_period :
# Calculate target velocity and position values and publish the commands
with self . _cmd_lock :
t_speed , t_left_steering , t_right_steering = self . calc_target_speed_steering ( delta_t )
self . publish_commands ( t_speed , t_left_steering , t_right_steering )
last_time = t
rate . sleep ()
will also calculate target speed and steering, ignore numbers out of bounds stabilize a stopped car
def calc_target_speed_steering ( self , delta_t ):
# don't go backwards for speed < 0
target_speed = max ( min ( self . max_speed , self . speed ), 0 )
target_steer_angle = max ( min ( self . max_steering_angle , self . steering_angle ), - self . max_steering_angle )
tanSteer = math . tan ( target_steer_angle )
t_left_steering = math . atan2 ( tanSteer , 1.0 - self . _wheel_separation / 2.0 / self . _wheel_base * tanSteer )
t_right_steering = math . atan2 ( tanSteer , 1.0 + self . _wheel_separation / 2.0 / self . _wheel_base * tanSteer )
t_speed = target_speed / self . _wheel_radius
if self . steering_angle_velocity == 0 :
t_left_steering = t_right_steering = target_steer_angle
# when speed==0 center wheels, else a car will spin
if t_speed == 0 :
t_left_steering = t_right_steering = 0
return t_speed , t_left_steering , t_right_steering
and will finally publish wheel and steering commands to the corresponding command topics
def publish_commands ( self , t_speed , t_left_steering , t_right_steering ):
'''Publishes the given action to all the topics in the given dicts
velocity_pub_dict - Dictionary containing all the velocity joints
steering_pub_dict - Dictionary containing all the movable joints
t_left_steering, t_right_steering - Desired amount, in radians, to move the movable joints by
t_speed - Angular velocity which the velocity joints should rotate with
'''
for _ , pub in self . _velocity_pub_dict_ . items ():
pub . publish ( t_speed )
self . _steering_pub_dict_ [ 'left' ]. publish ( t_left_steering )
self . _steering_pub_dict_ [ 'right' ]. publish ( t_right_steering )
Next: Teleoperation