Walkthrough: ROS Parameters and the wanderbot
Table of Contents
1 Submission Instructions
Just like the first walkthrough, this walkthrough will be submitted via Classroom for GitHub. See Canvas for the secret link for creating your private repository.
All deliverables described below should be pushed to this repository, and a URL similar to https://github.com/ME495-EmbeddedSystems/walkthrough2-f2018-YOUR-USERNAME should be submitted via Canvas before the due date.
1.1 Deliverables
Just like the first walkthrough, the goal of this assignment is to follow all of the steps below and understand all of the details. The GitHub link that you submit to me should contain a single ROS package that accomplishes the following goals:
- Demonstrates Proper Git Usage
- This means the repository should only track the relevant files for the single package used during this assignment (no extra files or packages), and there should be multiple commits.
- Illustrate Completion of Steps
- You need to show me that you've actually done all of the steps. This likely includes a README illustrating where I can find the evidence that all steps were completed. For example you could provide links to commits, tags, branches, or simply files.
- Following ROS Standards
- Your package should be organized like other ROS
packages, proper naming conventions should be used, you should have a
nicely formatted
package.xml
andCMakeLists.txt
, and you should attempt to set proper dependencies.
2 Wanderbot
2.1 Introduction
This tutorial is going to contain multiple examples building off what we've learned so far. For the most part we're going to be writing controllers for the TurtleBot in Gazebo. We'll start small and build from there.
One thing to note is that we use command velocities to make the TurtleBot
move (just like the turtlesim
package used in many ROS tutorials). We do
that using messages of type geometry_msgs/Twist
which contains six numbers,
organized into two vectors called linear
and angular
of type float64
.
This is what the output of rosmsg show geometry_msgs/Twist
looks like.
geometry_msgs/Vector3 linear float64 x float64 y float64 z geometry_msgs/Vector3 angular float64 x float64 y float64 z
2.2 Setup
To begin we'll create a new package in our workspace.
cd catkin_ws/src catkin_create_pkg wanderbot rospy geometry_msgs sensor_msgs cd .. catkin_make
2.3 red_light_green_light.py
Our first node will toggle the TurtleBot between moving forward and stopping every 3 seconds.
#!/usr/bin/env python import rospy from geometry_msgs.msg import Twist def main(): rospy.init_node('red_light_green_light') cmd_vel_pub = rospy.Publisher('cmd_vel', Twist, queue_size=1) red_light_twist = Twist() #1 green_light_twist = Twist() green_light_twist.linear.x = 0.25 #2 driving_forward = False light_change_time = rospy.Time.now() rate = rospy.Rate(10) while not rospy.is_shutdown(): if driving_forward: cmd_vel_pub.publish(green_light_twist)#3 rospy.loginfo('Moving') else: cmd_vel_pub.publish(red_light_twist)#3 rospy.loginfo('Stopped') if light_change_time < rospy.Time.now(): #4 rospy.logwarn('Switch') driving_forward = not driving_forward light_change_time = rospy.Time.now() + rospy.Duration(3) #5 rate.sleep() if __name__=='__main__': try: main() except KeyboardInterrupt: print ("Shutting Down")
2.3.1 Notes on red_light_green_light
comments
- The message constructor sets all fields to zero by default
- This makes the TurtleBot drive in a straight line forward at 0.25 \(m/s\).
- Publish the command velocities to achieve the two robot states we want
- Checks the time and toggle between the red and green light
- This allows us to change states after three seconds
2.3.2 Further explanations for red_light_green_light
- First start the TurtleBot simulator with
roslaunch turtlebot_gazebo turtlebot_world.launch
, and then in another terminal run thered_light_green_light.py
node, and in a third terminal runrostopic list
. Using tools likerostopic info
andrqt_graph
we can discover that the simulator is subscribing to the/cmd_vel_mux/input/teleop
topic. - If we run
rostopic type /cmd_vel_mux/input/teleop
we see that it's of typegeometry_msgs/Twist
. The TurtleBot expects command velocities on that topic, however we've named our topic in the nodecmd_vel
. To get ourcmd_vel
to publish to the right topic we will remap it. Remapping allows us to, at runtime, change what topics and services a node is looking for and providing. - In order to remap the
/cmd_vel
topic, we can use a command likerosrun wanderbot red_light_green_light.py cmd_vel:=cmd_vel_mux/input/teleop
We see that the TurtleBot moves for three seconds, then stops for three. If we want to see what this looks like in Gazebo we runroslaunch turtlebot_gazebo turtlebot_world.launch
like we did in the location monitor example in the previous Walkthrough. - This is a pretty dumb robot. It just moves and stops and doesn't care
about the world around it. Technically you can argue it's not a robot
because it's only computing and actuating while not sensing anything.
We'll change that in the next section. The TurtleBot comes equipped with a
Mirosoft Kinect. The Kinect is capable of producing full 3D
representations of data called point clouds. Point clouds are a bit too
complicated for right now. So we will instead work with a small fraction
of the data. The TurtleBot drivers publish a narrow horizontal slice of
the depth data as a
sensor_msgs/LaserScan
message on thescan
topic. The form of this data is similar to the data that you'd get from a laser scanner. It differs from real laser scans by having a smaller maximum range and more limited field of view.
2.4 wander.py
Our next node will make the TurtleBot will drive in a straight line until it detects an obstacle within 1.2m. If it doesn't detect an obstacle after 10 seconds it will stop moving forward and spin for five seconds before moving forward again.
#!/usr/bin/env python import rospy import math from geometry_msgs.msg import Twist from sensor_msgs.msg import LaserScan #1 class Wandering(object): def __init__(self): ''' Initialize parameters''' rospy.init_node('wander') self.cmd_vel_pub = rospy.Publisher('cmd_vel', Twist, queue_size=1) self.twist = Twist() self.state_change_time = rospy.Time.now() #2 self.driving_forward = True #2 self.scan_sub = rospy.Subscriber('scan', LaserScan, self.scan_callback) return def scan_callback(self,msg): # convert any NaN values to laser max range: ranges = [x if not math.isnan(x) else msg.range_max for x in msg.ranges] range_ahead = min(ranges) #3 rospy.loginfo("range ahead: %0.1f" % range_ahead) if self.driving_forward: #4 self.twist.linear.x = 0.2 self.twist.angular.z = 0 rospy.loginfo('Moving') if range_ahead < 1.2 or rospy.Time.now() > self.state_change_time: #5 self.driving_forward = False self.state_change_time = rospy.Time.now() + rospy.Duration(5) #6 else: self.twist.angular.z = 1 #7 self.twist.linear.x = 0 rospy.loginfo('Spinning') if rospy.Time.now() > self.state_change_time: #8 self.driving_forward = True rospy.loginfo('Switch') self.state_change_time = rospy.Time.now() + rospy.Duration(10) #9 self.cmd_vel_pub.publish(self.twist) #10 return def main(): wandering = Wandering() try: rospy.spin() except KeyboardInterrupt: print("Shutting down") if __name__=='__main__': main()
2.4.1 Notes on wander
comments
- Import the LaserScan message since we'll be using it
- These are the two variables we'll use for our controller logic, driving state and time
- This stores the minimum range our simulated laser scanner detects. We
could decide to use the middle element of the ranges array instead with
range_ahead = msg.ranges[len(msg.ranges)/2]
. Alternatively we could usemsg.range_min
to use the minimum value the laser scanner is capable of sensing (assuming our driver has properly set this). Think about how these choices would affect the TurtleBot's behavior? - Set the command velocities for moving forward at 0.2 \(m/s\)
- If there's no obstacle within 1.2m or the robot has been moving for 10 seconds, stop moving.
- Stop moving for 5 seconds.
- Spin on the spot for 5 seconds
- Check that 5 seconds have elapsed
- Change states after 10 seconds
- Publish the messages
2.4.2 Further explanations and explorations for wander
- Don't forget to remap the
cmd_vel
topic again. - Inspect the
sensor_msgs/LaserScan
message and/scan
topic. Can you understand what all the fields in the message mean? - Can you think of a better algorithm for the TurtleBot to explore it's environment without hitting anything or driving off into nowhere?
- Try adding a ROS Timer to periodically read the robot's state (spinning vs moving), and print out the robot's state to the screen when a state change is detected. Also calculate how many seconds elapsed before the robot changed state.
3 Parameters and Names
3.1 Names, namespaces and remapping
Central to ROS parameters and the parameter server is the concept of names.
ROS uses the forward slash (/
) to delimit namespaces. Just the same way two
files named readme.txt
can exist in multiple paths such as
/home/nathan/readme.txt
and /home/drake/readme.txt
. Also analogous to
your filesystem the /
namespace is the root namespace. Namespaces are
powerful because one can start identical nodes in separate namespaces and
avoid name collisions.
However, we don't always want or need namespace encapsulation. There are
sometimes situations that require nodes in different namespaces to interact
with one another using a common topic. This problem is easily solved by
remapping. Similar to what we did in the wanderbot example where we remapped
cmd_vel
to /cmd_vel_mux/input/teleop
. The turtlebot was expecting
messages on /cmd_vel_mux/input/teleop
topic — so we had two options,
either remap or modify the source code to specifically publish the
/cmd_vel_mux/input/teleop
topic. Clearly the remapping solution is more
versatile and extensible.
Similar to how we can push a topic into the namespace of another topic we can
do the same with nodes using the ___ns
namespace-remapping syntax of
rosrun
. For example:
rosrun wanderbot talker.py __ns:=different_talker
The output of rosnode list
will be
/different_talker/talker_24817_1474393340742 /rosout
which shows that our talker is now in the namespace different_talker
.
We can also change the name of a node using the ___name
remapping syntax of
rosrun
. This allows us to launch multiple instances of the same node with
different names - and parameters if need be. In two different tabs run
rosrun wanderbot wander.py __name:=talker1
and rosrun wanderbot wander.py
__name:=talker2
. In a third tab run rosnode list
. The output should look
like
/rosout /talker1 /talker2
3.2 Parameters introduction
ROS parameters exist as a sort of ROS-global list of variables. Any node can
query and set these parameters. Typically these are used for defining
configuration options for one or more nodes. In this way, parameters can
replace requesting command line input by using input()
or raw_input()
functions in your node and they can act as command line arguments that can
automatically be read by multiple nodes. A parameter has a name and a value
and the parameter server keeps track of all of the parameters.
3.2.1 The parameter server
A parameter server is a shared, multi-variate dictionary that is accessible via network APIs… this simply means it's a fancy dictionary. Nodes use the server to store and retrive parameters while running. Which means you can tweak values of a parameter and have multiple instances of the same node with differing functionalities. The example on the ROS wiki uses a situation where you want to give different cameras different exposure settings but still use the same driver node.
The parameter server can store strings, integers, floats, booleans, lists,
dictionaries, iso8601 dates, and base64-encoded data. One thing to note is
that dictionaries must have string
keys. Parameters must be actively
queried by the nodes that are interested in their values, they are most
suitable for configuration information that will not change (much) over
time. If you are interested in having your node automatically run a callback
when a parameter changes value (instead of constantly polling), you should
look into dynamic_reconfigure. Parameters may be defined in a YAML file and
then loaded to the parameter server with rosparam load
or via the
rosparam
tag in launch files.
3.2.2 rosparam
rosparam
is a command-line tool for getting, setting, and deleting
parameters from the ROS Parameter Server. It also enables loading and
dumping Parameter Server state to a file.
Commands: rosparam set set parameter rosparam get get parameter rosparam load load parameters from file rosparam dump dump parameters to file rosparam delete delete parameter rosparam list list parameter names
3.2.3 Private parameters
Often in the ROS world, you'll see nodes use private parameters. A private parameter is still publicly accessible, like all parameters, however its fully-qualified name is formed by prepending the node's name to the parameter's name. By always having the name of the node come before the parameter value, it helps end users sort out which nodes are likely to be looking at a given parameter.
There are several ways of setting variables to initial values through
the use of the parameter server. One is through a launch file, and
another is from the command line. We set parameters at the command line
at runtime using the :=
syntax. The ROS node below illustrates the
concept of using private parameters.
3.3 talker_param.py
Let's add the following node to our wanderbot
package.
#!/usr/bin/env python import rospy from std_msgs.msg import String def talker(): rospy.init_node('talker_param', anonymous=True) pub = rospy.Publisher('chatter', String, queue_size=10) rate = rospy.Rate(10) #10hz msg = rospy.get_param('~name',default='Anybody') #1 while not rospy.is_shutdown(): hello_str = "Hello I'm %s %s" % (msg,rospy.get_time()) rospy.loginfo(hello_str) pub.publish(hello_str) rate.sleep() if __name__ == '__main__': try: talker() except rospy.ROSInterruptException: pass
Note that line #1
gets the private parameter name
or defaults to
"Anybody" if that parameter is not available.
If we simply
rosrun wanderbot talker_param.py
the output will be something like[INFO] [WallTime: 1474398453.585985] Hello I'm Anybody 1474398453.59 [INFO] [WallTime: 1474398453.683912] Hello I'm Anybody 1474398453.68 [INFO] [WallTime: 1474398453.783701] Hello I'm Anybody 1474398453.78 [INFO] [WallTime: 1474398453.883998] Hello I'm Anybody 1474398453.88 [INFO] [WallTime: 1474398453.983801] Hello I'm Anybody 1474398453.98 [INFO] [WallTime: 1474398454.084103] Hello I'm Anybody 1474398454.08
However if we specify a name
rosrun wanderbot talker_param.py _name:=CiromaChukwumaAdekunle
the output will be[INFO] [WallTime: 1474398593.321342] Hello I'm CiromaChukwumaAdekunle 1474398593.32 [INFO] [WallTime: 1474398593.420146] Hello I'm CiromaChukwumaAdekunle 1474398593.42 [INFO] [WallTime: 1474398593.520260] Hello I'm CiromaChukwumaAdekunle 1474398593.52 [INFO] [WallTime: 1474398593.620027] Hello I'm CiromaChukwumaAdekunle 1474398593.62 [INFO] [WallTime: 1474398593.720456] Hello I'm CiromaChukwumaAdekunle 1474398593.72 [INFO] [WallTime: 1474398593.820643] Hello I'm CiromaChukwumaAdekunle 1474398593.82 [INFO] [WallTime: 1474398593.920661] Hello I'm CiromaChukwumaAdekunle 1474398593.92
Note that we use
~
in the code to indicate that the node's name should be appended to the front ofname
(resulting in/talker_param/name
for the fully-qualified name), but at the command line, we must use_
. This is to avoid conflicts with the fact that many shells use the~
symbol as an alias for a user's home directory.
3.4 A more complex example
In this example, we're going to drive the turtlebot with keystrokes from the
keyboard. To do this we'll publish the keystrokes as std_msgs/String
messages on the key
topic. Pressing on a key should cause the robot to move
forward, backwards, turn left, turn right and stop by sending command
velocities to the robot. However, in a real robot, sending instantaneous
changes in velocity may result in high accelerations that cause skidding,
belts slipping, motor over-currents, and other unpleasantness. So we will
smooth out our control smoothly ramping between desired velocities.
Both of the following nodes should be in your wanderbot
package.
3.4.1 key_publisher.py
This is a simple ROS node that logs the first key stroke from the
keyboard and publishes them to the keys
topic. We'll be subscribing to
it and using it's output to teleoperate the TurtleBot.
#!/usr/bin/env python import sys,select,tty,termios import rospy from std_msgs.msg import String if __name__=='__main__': rospy.init_node("keyboard_driver") key_pub = rospy.Publisher('keys', String, queue_size=1) rate = rospy.Rate(100) #save attributes old_attr = termios.tcgetattr(sys.stdin) tty.setcbreak(sys.stdin.fileno()) print "Publishing keystrokes. Press Ctrl-C to exit..." while not rospy.is_shutdown(): if select.select([sys.stdin],[],[],0)[0] == [sys.stdin]: key_pub.publish(sys.stdin.read(1)) rate.sleep() #Reset console to standard mode termios.tcsetattr(sys.stdin,termios.TCSADRAIN,old_attr)
3.4.2 keys_to_twist_with_ramps.py
In this ROS node we subscribe to the keys topic and use it to teleoperate the TurtleBot. We also scale and ramp the velocities and accelerations being sent to the TurtleBot
#!/usr/bin/env python import rospy import math from std_msgs.msg import String from geometry_msgs.msg import Twist key_mapping = {'w':[0,1],'a':[1,0],'s':[0,-1],'d':[-1,0],'x':[0,0]} #1 class KeysTwistWithRamp(object): ''' Ramp Motion Commands ''' def __init__(self): rospy.init_node('keys_to_twist_with_ramps') self.g_twist_pub = rospy.Publisher('cmd_vel', Twist, queue_size=1) self.g_last_twist = Twist() self.g_target_twist = Twist() self.g_last_twist_send_time = rospy.Time.now() self.rate = rospy.Rate(20) #2 self.g_vel_scales = [0.1, 0.1] #3 self.g_vel_ramps = [1,1] #4 def ramped_vel(self, v_prev, v_target, t_prev, t_now, ramp_rate): step = ramp_rate * (t_now - t_prev).to_sec() #5 sign = 1.0 if (v_target > v_prev) else -1.0 error = math.fabs(v_target-v_prev) if error < step: #6 return v_target else: return v_prev + sign * step #7 def ramped_twist(self, prev, target, t_prev, t_now, ramps): #8 tw = Twist() tw.angular.z = self.ramped_vel(prev.angular.z, target.angular.z, t_prev, t_now, ramps[0]) tw.linear.x = self.ramped_vel(prev.linear.x, target.linear.x, t_prev, t_now, ramps[1]) return tw def send_twist(self): #9 t_now = rospy.Time.now() self.g_last_twist = self.ramped_twist(self.g_last_twist,self.g_target_twist,self.g_last_twist_send_time,t_now,self.g_vel_ramps) self.g_last_twist_send_time = t_now #10 self.g_twist_pub.publish(self.g_last_twist) def keys_callback(self, msg): if len(msg.data) == 0 or not key_mapping.has_key(msg.data[0]): return #11 vels = key_mapping[msg.data[0]] #12 self.g_target_twist.angular.z = vels[0] * self.g_vel_scales[0] self.g_target_twist.linear.x = vels[1] * self.g_vel_scales[1] def fetch_param(self,name,default): #13 if rospy.has_param(name): #14 return rospy.get_param(name) else: rospy.logwarn("parameter %s not defined. Defaulting to %.3f" % (name, default)) #15 return default def main(): key_pub_rate = KeysTwistWithRamp() rospy.Subscriber('keys', String, key_pub_rate.keys_callback) #16 key_pub_rate.g_vel_scales[0] = key_pub_rate.fetch_param('~angular_scale', 0.1) key_pub_rate.g_vel_scales[1] = key_pub_rate.fetch_param('~linear_scale', 0.1) key_pub_rate.g_vel_ramps[0] = key_pub_rate.fetch_param('~angular_accel', 1.0) key_pub_rate.g_vel_ramps[1] = key_pub_rate.fetch_param('~linear_accel', 1.0) while not rospy.is_shutdown(): key_pub_rate.send_twist() key_pub_rate.rate.sleep() if __name__=='__main__': main()
3.4.3 Notes on comments in keys_to_twist_with_ramps
- A dictionary that maps key presses to movement
- w: Forward,
- s: Backwards,
- x: stop key
- a: rotate clockwise,
- d: anti-clockwise
- Publish the Twist message to the topic at 20Hz
- Initialize the scale at a small value. Scaling in theory allows us to use this car on multiple platforms. 1 \(m/s\) is fast for a TurtleBot, however it's slow for cars or a bigger mobile platforms.
- Units: \(m/s^2\)
- Convert time to seconds
- We can get there within this timestep we're done
- Take a step toward the target
- In this function we instantiate a new twist. We then ramp the velocities coming in and split it into linear and angular velocities and return the new twist with the ramped velocities.
- In this function,the ramp is applied to the twist message being published
- Reset current time for next function call
- Return nothing if the key pressed isn't in
key_mapping
- Take the first key pressed as the key for the dictionary and assign the
value to
vels
. Remember the dictionary command velocities are stored as a list - A function to get parameter names or use default values
- Check for the existence of parameters and fetch parameter value from parameter server if they exist
- Warn if we don't have any parameters and return the default value we
specify.
logwarn()
is a ROS logging cal that prints colourized text to the screen which is useful in debugging. Other logging calls areloginfo()
,logwarn()
,logerr()
andlogfatal()
- Subscribe to the keys topic and set the scale and ramp values to the parameter values(either the ones we specify or the default ones)
3.4.4 Running everything
To make everything easier to start, we are going to use a launch file.
Create a directory in your package called launch/
. Then create a file
called keys_to_twist_with_ramps.launch
and fill it in with the content
below. Once done, you'll be able to launch the file with roslaunch
wanderbot keys_to_twist_with_ramps.launch
. Note that this launch file
defaults to starting the simulator as well. If you want this behavior, be
sure to stop any already-running Gazebo instances you have. If you'd like to
prevent this launch file from starting the simulator, pass sim:=false
at
the end of your roslaunch
invocation.
Also note that the launch-prefix="xterm -e"
attribute is used when
starting the key_publisher.py
node. This causes a new window to pop up
running the terminal emulator xterm. We do this because it is convenient
to be able to have a separate window for parsing just your keyboard
commands. We use xterm instead of the default terminal emulator (which is
called gnome-terminal) because it is a basic terminal emulator that starts
up very quickly. Note that for this line to work you'll need to have xterm
installed. You could check if it is installed with dpkg -l |grep xterm
. If
it is not installed, you can install it with sudo apt install xterm
.
In another terminal run rqt_plot cmd_vel_mux/input/teleop/linear/x
cmd_vel_mux/input/teleop/angular/z
to see how the velocities are modulated.
keys_to_twist_with_ramps.launch
<launch> <arg name="sim" default="true" doc="set to true to start TurtleBot simulator"/> <node name="keys_to_twist_with_ramps" pkg="wanderbot" type="keys_to_twist_with_ramps.py"> <remap from="cmd_vel" to="cmd_vel_mux/input/teleop"></remap> <param name="linear_scale" value="0.5" /> <param name="angular_scale" value="1.0"/> <param name="linear_accel" value="1.0" /> <param name="angular_accel" value="1.0"/> </node> <node name="keys" pkg="wanderbot" type="key_publisher.py" launch-prefix="xterm -e"/> <include if="$(arg sim)" file="$(find turtlebot_gazebo)/launch/turtlebot_world.launch" /> </launch>
Play around with driving the TurtleBot and observe how your
rqt_plot
changes.
4 References and Further Reading
- ROS Parameter Server documentation
- Understanding Services and Params from the ROS Wiki
- Much of the content for these scripts was borrowed from or inspired by Programming Robots with ROS by Quigley, Gerkey, and Smart