Week 3 'move_square' Python Template
A combined publisher-subscriber node to achieve odometry-based control...
Below you will find a template Python script to show you how you can both publish to /cmd_vel
and subscribe to /odom
in the same node. This will help you build a closed-loop controller to make your robot follow a square motion path of size: 1m x 1m.
You can publish velocity commands to /cmd_vel
to make the robot move, monitor the robot's position and orientation in real-time, determine when the desired movement has been completed, and then update the velocity commands accordingly.
Suggested Approach¶
Moving in a square can be achieved by switching between two different movement states sequentially: Moving forwards and turning on the spot. At the start of each movement step we can read the robot's current odometry, and then use this as a reference to compare to, and to tell us when the robot's position/orientation has changed by the required amount, e.g.:
- With the robot stationary, read the odometry to determine its current X and Y position in the environment.
- Move forwards until the robot's X and Y position indicate that it has moved linearly by 0.5m.
- Stop moving forwards.
- Read the robot's odometry to determine its current orientation ("yaw"/
θz
). - Turn on the spot until the robot's orientation changes by 90°.
- Stop turning.
- Repeat.
import rospy
# import the Twist message for publishing velocity commands:
from geometry_msgs.msg import Twist
# import the Odometry message for subscribing to the odom topic:
from nav_msgs.msg import Odometry
# import the function to convert orientation from quaternions to angles:
from tf.transformations import euler_from_quaternion
# import some useful mathematical operations (and pi), which you may find useful:
from math import sqrt, pow, pi
class Square():
def callback_function(self, topic_data: Odometry):
# obtain relevant topic data: pose (position and orientation):
pose = topic_data.pose.pose
position = pose.position
orientation = pose.orientation
# obtain the robot's position co-ords:
pos_x = position.x
pos_y = position.y
# convert orientation co-ords to roll, pitch & yaw
# (theta_x, theta_y, theta_z):
(roll, pitch, yaw) = euler_from_quaternion(
[orientation.x, orientation.y, orientation.z, orientation.w], "sxyz"
)
# We're only interested in x, y and theta_z
# so assign these to class variables (so that we can
# access them elsewhere within our Square() class):
self.x = pos_x
self.y = pos_y
self.theta_z = yaw
# If this is the first time that the callback_function has run
# (e.g. the first time a message has been received), then
# obtain a "reference position" (used to determine how far
# the robot has moved during its current operation)
if self.startup:
# don't initialise again:
self.startup = False
# set the reference position:
self.x0 = self.x
self.y0 = self.y
self.theta_z0 = self.theta_z
def __init__(self):
node_name = "move_square"
# a flag if this node has just been launched
self.startup = True
# This might be useful in the main_loop() (to switch between
# turning and moving forwards)
self.turn = False
# setup a '/cmd_vel' publisher and an '/odom' subscriber:
self.pub = rospy.Publisher("cmd_vel", Twist, queue_size=10)
self.sub = rospy.Subscriber("odom", Odometry, self.callback_function)
rospy.init_node(node_name, anonymous=True)
self.rate = rospy.Rate(10) # hz
# define the robot pose variables and initialise them to zero:
# variables for the robot's "current position":
self.x = 0.0
self.y = 0.0
self.theta_z = 0.0
# variables for a "reference position":
self.x0 = 0.0
self.y0 = 0.0
self.theta_z0 = 0.0
# define a Twist message instance, to set robot velocities
self.vel = Twist()
self.ctrl_c = False
rospy.on_shutdown(self.shutdownhook)
rospy.loginfo(f"the {node_name} node has been initialised...")
def shutdownhook(self):
# publish an empty twist message to stop the robot
# (by default all velocities will be zero):
self.pub.publish(Twist())
self.ctrl_c = True
def main_loop(self):
while not self.ctrl_c:
# here is where your code would go to control the motion of your
# robot. Add code here to make your robot move in a square of
# dimensions 1 x 1m...
# publish whatever velocity command has been set in your code above:
self.pub.publish(self.vel)
# maintain the loop rate @ 10 hz
self.rate.sleep()
if __name__ == "__main__":
node = Square()
try:
node.main_loop()
except rospy.ROSInterruptException:
pass
Alternative Approach: Waypoint Tracking¶
A square motion path can be fully defined by the co-ordinates of its four corners, and we can make the robot move to each of these corners one-by-one, using its odometry system to monitor its real-time position, and adapting linear and angular velocities accordingly.
This is slightly more complicated, and you might want to wait until you have a bit more experience with ROS before tackling it this way.