Note: This tutorial requires basic understanding of the ROS action library.
(!) Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags.

Moving the arm using the Joint Trajectory Action

Description: This tutorial demonstrates moving the arm using the Joint Trajectory Action which is an interface to the lower-level Joint Trajectory Controller.

Keywords: moving move arm joint controller trajectory action pr2

Tutorial Level:

Overview and prerequisites

In this tutorial we will focus on moving the arm using an action interface to the low-level Joint Trajectory Controller. Note that, at this level, we are not concerned with many aspects of arm movement, such as:

  • generation of trajectories with smooth velocity and acceleration profiles
  • obstacle avoidance or motion planning

Those components can be found in a higher level package called move_arm. However, if move_arm is not available, or you want to be using a lower-level interface, this tutorial shows the way.

Commanding a joint trajectory for the arm requires the following components:

  • a controller that sends commands directly to the joints;

  • an action interface to the controller in the form of a ROS action, that takes in a trajectory command expressed as a series of joint angles and sends the appropriate low-level commands to the controller;

  • the high-level program that uses the action interface to issue the desired joint trajectories.

The first two components are available in ROS. In this tutorial we will show you how to use them by writing the third component, the high level program.

Before you begin, bring up a robot, either on the hardware or in gazebo.

The Joint Trajectory Controller

The arm trajectory controller is brought up on robot start-up. In general, a controller can be in one of three states:

  • not loaded
  • loaded, but not running (stopped)
  • running

In general, you can use the pr2_controller_manager to check which controllers are available. After you bring up the robot, use the following command:

rosrun pr2_controller_manager pr2_controller_manager list

In the list that is printed, look for a line starting with r_arm_controller . If you find:

  • r_arm_controller (running) : the controller is running; you can skip to the next section of this tutorial

  • r_arm_controller (stopped) : the controller is loaded, but not running. To start it, use again the pr2_controller_manager (the arm will start to hold its current angles) :

    rosrun pr2_controller_manager pr2_controller_manager start r_arm_controller
  • no mention of r_arm_controller : the controller has not been loaded; this probably means that something went wrong during robot start-up. Abort this tutorial and investigate the cause for that.

Note that this usage of the pr2_controller_manager also applies to other controllers, such as the head trajectory controller or the gripper controller.

The Joint Trajectory Action Interface

The most convenient way of sending commands to the arm controller is through the JointTrajectoryAction, which is an implementation of a ROS action. This action takes in commands in the form of desired arm trajectories, then executes them by commanding the controller directly. For more details about actions in ROS see the actionlib package.

Note that the examples here will be given in C++, but the ROS action interface also works with Python.

The node that implements this action interface is started automatically on robot start up. To check, look at the list of topics in the r_arm_controller namespace after you bring up the robot:

rostopic list r_arm_controller

In the list of topics, you should see lines containing /r_arm_controller/joint_trajectory_action/. If they are not present, it means that either the action node was not started on robot start-up, or that it has been stopped. Abort this tutorial and investigate the cause for that.

The trajectory message

The joint trajectory action interface receives goals in the form of trajectory_msgs/JointTrajectory messages, each of which describes the trajectory that the controller will attempt to execute.

This message contains:

  • a header

  • a set of joint names

  • a set of waypoints , each of them containing the desired position, velocity and acceleration at each joint. It is assumed that the values in the waypoints are ordered the same way as in the set of joint names contained in the message.

Note that if you only want to specify a trajectory through joint positions, you should still fill in the velocities fields with zeroes.

Set of joint names for the goal message. Both the Joint Trajectory Controller and the Joint Trajectory Action have an internal list of joints that they are operating on. This list is set when the controller and the action are started. After that, both the controller and the action expect any command to specify desired values for all of the joints they control.

By default, both the controller and the action are started up with a list of joints containing all the joints of the arm. Changing this behavior is beyond the scope of this tutorial; however, you can find out what this list of joints is by querying the ROS parameter server. You can query either the controller or the action, the lists should be similar:

rosparam get /r_arm_controller/joints

or

rosparam get /r_arm_controller/joint_trajectory_action/joints

The result should look like this:

[r_shoulder_pan_joint, r_shoulder_lift_joint, r_upper_arm_roll_joint, r_elbow_flex_joint, r_forearm_roll_joint, r_wrist_flex_joint, r_wrist_roll_joint]

The punchline is that any goal message that you send to the action must contain all of these joints (and no others) inside its joint names field. Implicitly, this also means that you must specify desired values for them in each waypoint. If these conditions are not met, the goal will be rejected as invalid. See the code example below for an example of a goal trajectory containing all required the joint names.

Time management of the trajectory. The JointTrajectory message has two fields for managing time:

  • the stamp field in the header of the message

  • a time_from_start value for each waypoint.

Both of these are important.

The stamp in the header determines when the execution of the trajectory starts. Once the time in the stamp field passes, the controller will begin to move towards the first waypoint in the trajectory. The controller attempts to achieve each waypoint at the time obtained by adding that waypoint's time_from_start value to the time in stamp. The time_from_start values must be monotonically non-decreasing or else the trajectory will be considered invalid.

For each point in the trajectory, put the desired joint state into positions and velocities. The controller will interpolate between these points using cubic splines. You may optionally fill in accelerations if you wish for the controller to use quintic splines. The controller will attempt to follow whatever trajectory you specify, so it is your responsibility to ensure that the desired trajectory is smooth.

Setting up your package

We will now prepare our own package for writing code to move the arm. Using the roscreate-pkg tool, you can conveniently create a package as follows:

roscreate-pkg simple_trajectory actionlib roscpp pr2_controllers_msgs

Note that we have a few dependencies for our package. One of them is roscpp as we will be writing the example in this tutorial in C++. We also depend on actionlib since we are using an action interface, and on pr2_controllers_msgs which defines the goal message we use to communicate with the action.

After creating your package, we'll need to roscd to it, since we'll be using it as our workspace.

roscd simple_trajectory

Creating the node

Put the following into src/simple_trajectory.cpp:

   1 #include <ros/ros.h>
   2 #include <pr2_controllers_msgs/JointTrajectoryAction.h>
   3 #include <actionlib/client/simple_action_client.h>
   4 
   5 typedef actionlib::SimpleActionClient< pr2_controllers_msgs::JointTrajectoryAction > TrajClient;
   6 
   7 class RobotArm
   8 {
   9 private:
  10   // Action client for the joint trajectory action 
  11   // used to trigger the arm movement action
  12   TrajClient* traj_client_;
  13 
  14 public:
  15   //! Initialize the action client and wait for action server to come up
  16   RobotArm() 
  17   {
  18     // tell the action client that we want to spin a thread by default
  19     traj_client_ = new TrajClient("r_arm_controller/joint_trajectory_action", true);
  20 
  21     // wait for action server to come up
  22     while(!traj_client_->waitForServer(ros::Duration(5.0))){
  23       ROS_INFO("Waiting for the joint_trajectory_action server");
  24     }
  25   }
  26 
  27   //! Clean up the action client
  28   ~RobotArm()
  29   {
  30     delete traj_client_;
  31   }
  32 
  33   //! Sends the command to start a given trajectory
  34   void startTrajectory(pr2_controllers_msgs::JointTrajectoryGoal goal)
  35   {
  36     // When to start the trajectory: 1s from now
  37     goal.trajectory.header.stamp = ros::Time::now() + ros::Duration(1.0);
  38     traj_client_->sendGoal(goal);
  39   }
  40 
  41   //! Generates a simple trajectory with two waypoints, used as an example
  42   /*! Note that this trajectory contains two waypoints, joined together
  43       as a single trajectory. Alternatively, each of these waypoints could
  44       be in its own trajectory - a trajectory can have one or more waypoints
  45       depending on the desired application.
  46   */
  47   pr2_controllers_msgs::JointTrajectoryGoal armExtensionTrajectory()
  48   {
  49     //our goal variable
  50     pr2_controllers_msgs::JointTrajectoryGoal goal;
  51 
  52     // First, the joint names, which apply to all waypoints
  53     goal.trajectory.joint_names.push_back("r_shoulder_pan_joint");
  54     goal.trajectory.joint_names.push_back("r_shoulder_lift_joint");
  55     goal.trajectory.joint_names.push_back("r_upper_arm_roll_joint");
  56     goal.trajectory.joint_names.push_back("r_elbow_flex_joint");
  57     goal.trajectory.joint_names.push_back("r_forearm_roll_joint");
  58     goal.trajectory.joint_names.push_back("r_wrist_flex_joint");
  59     goal.trajectory.joint_names.push_back("r_wrist_roll_joint");
  60 
  61     // We will have two waypoints in this goal trajectory
  62     goal.trajectory.points.resize(2);
  63 
  64     // First trajectory point
  65     // Positions
  66     int ind = 0;
  67     goal.trajectory.points[ind].positions.resize(7);
  68     goal.trajectory.points[ind].positions[0] = 0.0;
  69     goal.trajectory.points[ind].positions[1] = 0.0;
  70     goal.trajectory.points[ind].positions[2] = 0.0;
  71     goal.trajectory.points[ind].positions[3] = 0.0;
  72     goal.trajectory.points[ind].positions[4] = 0.0;
  73     goal.trajectory.points[ind].positions[5] = 0.0;
  74     goal.trajectory.points[ind].positions[6] = 0.0;
  75     // Velocities
  76     goal.trajectory.points[ind].velocities.resize(7);
  77     for (size_t j = 0; j < 7; ++j)
  78     {
  79       goal.trajectory.points[ind].velocities[j] = 0.0;
  80     }
  81     // To be reached 1 second after starting along the trajectory
  82     goal.trajectory.points[ind].time_from_start = ros::Duration(1.0);
  83 
  84     // Second trajectory point
  85     // Positions
  86     ind += 1;
  87     goal.trajectory.points[ind].positions.resize(7);
  88     goal.trajectory.points[ind].positions[0] = -0.3;
  89     goal.trajectory.points[ind].positions[1] = 0.2;
  90     goal.trajectory.points[ind].positions[2] = -0.1;
  91     goal.trajectory.points[ind].positions[3] = -1.2;
  92     goal.trajectory.points[ind].positions[4] = 1.5;
  93     goal.trajectory.points[ind].positions[5] = -0.3;
  94     goal.trajectory.points[ind].positions[6] = 0.5;
  95     // Velocities
  96     goal.trajectory.points[ind].velocities.resize(7);
  97     for (size_t j = 0; j < 7; ++j)
  98     {
  99       goal.trajectory.points[ind].velocities[j] = 0.0;
 100     }
 101     // To be reached 2 seconds after starting along the trajectory
 102     goal.trajectory.points[ind].time_from_start = ros::Duration(2.0);
 103 
 104     //we are done; return the goal
 105     return goal;
 106   }
 107 
 108   //! Returns the current state of the action
 109   actionlib::SimpleClientGoalState getState()
 110   {
 111     return traj_client_->getState();
 112   }
 113  
 114 };
 115 
 116 int main(int argc, char** argv)
 117 {
 118   // Init the ROS node
 119   ros::init(argc, argv, "robot_driver");
 120 
 121   RobotArm arm;
 122   // Start the trajectory
 123   arm.startTrajectory(arm.armExtensionTrajectory());
 124   // Wait for trajectory completion
 125   while(!arm.getState().isDone() && ros::ok())
 126   {
 127     usleep(50000);
 128   }
 129   return 0;
 130 }

Building

Add the following line to the CMakeLists.txt:

rosbuild_add_executable(simple_trajectory src/simple_trajectory.cpp)

and type 'make' in the simple_trajectory directory to make your executable.

Running

Remember (from earlier in this tutorial) to make sure your robot is up (either in simulation or on hardware), the joint trajectory controller is running and the joint action node is also running.

Simply run your executable:

bin/simple_trajectory

The arm should move.

Wiki: pr2_controllers/Tutorials/Moving the arm using the Joint Trajectory Action (last edited 2020-07-20 12:47:32 by RobertZickler)