\(\def\dt{\Delta t}\)
\(\newcommand{\transpose}[0]{^\mathrm{T}}\)
\(\newcommand{\half}[0]{\tfrac{1}{2}}\)
\(\newcommand{\Half}[0]{\frac{1}{2}}\)
\(\newcommand{\norm}[1]{\left\lVert#1\right\rVert}\)
\(\newcommand\given[1][]{\:#1\vert\:}\)
UP | HOME

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 and CMakeLists.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

  1. The message constructor sets all fields to zero by default
  2. This makes the TurtleBot drive in a straight line forward at 0.25 \(m/s\).
  3. Publish the command velocities to achieve the two robot states we want
  4. Checks the time and toggle between the red and green light
  5. 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 the red_light_green_light.py node, and in a third terminal run rostopic list. Using tools like rostopic info and rqt_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 type geometry_msgs/Twist. The TurtleBot expects command velocities on that topic, however we've named our topic in the node cmd_vel. To get our cmd_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 like rosrun 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 run roslaunch 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 the scan 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

  1. Import the LaserScan message since we'll be using it
  2. These are the two variables we'll use for our controller logic, driving state and time
  3. 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 use msg.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?
  4. Set the command velocities for moving forward at 0.2 \(m/s\)
  5. If there's no obstacle within 1.2m or the robot has been moving for 10 seconds, stop moving.
  6. Stop moving for 5 seconds.
  7. Spin on the spot for 5 seconds
  8. Check that 5 seconds have elapsed
  9. Change states after 10 seconds
  10. 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 of name (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

  1. A dictionary that maps key presses to movement
    • w: Forward,
    • s: Backwards,
    • x: stop key
    • a: rotate clockwise,
    • d: anti-clockwise
  2. Publish the Twist message to the topic at 20Hz
  3. 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.
  4. Units: \(m/s^2\)
  5. Convert time to seconds
  6. We can get there within this timestep we're done
  7. Take a step toward the target
  8. 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.
  9. In this function,the ramp is applied to the twist message being published
  10. Reset current time for next function call
  11. Return nothing if the key pressed isn't in key_mapping
  12. 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
  13. A function to get parameter names or use default values
  14. Check for the existence of parameters and fetch parameter value from parameter server if they exist
  15. 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 are loginfo(), logwarn(), logerr() and logfatal()
  16. 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

  1. ROS Parameter Server documentation
  2. Understanding Services and Params from the ROS Wiki
  3. Much of the content for these scripts was borrowed from or inspired by Programming Robots with ROS by Quigley, Gerkey, and Smart
Creative Commons License
ME 495: Embedded Systems in Robotics by Jarvis Schultz is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.