cob_head_axis.cpp
Go to the documentation of this file.
00001 /****************************************************************
00002  *
00003  * Copyright (c) 2010
00004  *
00005  * Fraunhofer Institute for Manufacturing Engineering   
00006  * and Automation (IPA)
00007  *
00008  * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
00009  *
00010  * Project name: care-o-bot
00011  * ROS stack name: cob3_driver
00012  * ROS package name: cob_camera_axis
00013  *                                                              
00014  * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
00015  *                      
00016  * Author: Ulrich Reiser, email:ulrich.reiser@ipa.fhg.de
00017  *
00018  * Date of creation: Jan 2010
00019  * ToDo:
00020  *
00021  * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
00022  *
00023  * Redistribution and use in source and binary forms, with or without
00024  * modification, are permitted provided that the following conditions are met:
00025  *
00026  *       * Redistributions of source code must retain the above copyright
00027  *         notice, this list of conditions and the following disclaimer.
00028  *       * Redistributions in binary form must reproduce the above copyright
00029  *         notice, this list of conditions and the following disclaimer in the
00030  *         documentation and/or other materials provided with the distribution.
00031  *       * Neither the name of the Fraunhofer Institute for Manufacturing 
00032  *         Engineering and Automation (IPA) nor the names of its
00033  *         contributors may be used to endorse or promote products derived from
00034  *         this software without specific prior written permission.
00035  *
00036  * This program is free software: you can redistribute it and/or modify
00037  * it under the terms of the GNU Lesser General Public License LGPL as 
00038  * published by the Free Software Foundation, either version 3 of the 
00039  * License, or (at your option) any later version.
00040  * 
00041  * This program is distributed in the hope that it will be useful,
00042  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00043  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00044  * GNU Lesser General Public License LGPL for more details.
00045  * 
00046  * You should have received a copy of the GNU Lesser General Public 
00047  * License LGPL along with this program. 
00048  * If not, see <http://www.gnu.org/licenses/>.
00049  *
00050  ****************************************************************/
00051 
00052 //##################
00053 //#### includes ####
00054 
00055 // standard includes
00056 //--
00057 
00058 // ROS includes
00059 #include <ros/ros.h>
00060 #include <urdf/model.h>
00061 #include <actionlib/server/simple_action_server.h>
00062 
00063 // ROS message includes
00064 #include <sensor_msgs/JointState.h>
00065 //#include <pr2_controllers_msgs/JointTrajectoryAction.h>
00066 #include <control_msgs/FollowJointTrajectoryAction.h>
00067 #include <pr2_controllers_msgs/JointTrajectoryControllerState.h>
00068 #include <diagnostic_msgs/DiagnosticArray.h>
00069 
00070 // ROS service includes
00071 #include <cob_srvs/Trigger.h>
00072 #include <cob_srvs/SetOperationMode.h>
00073 #include <cob_srvs/SetDefaultVel.h>
00074 
00075 // external includes
00076 #include <cob_camera_axis/ElmoCtrl.h>
00077 
00078 //####################
00079 //#### node class ####
00080 class NodeClass
00081 {
00082         //
00083         public:
00084         // create a handle for this node, initialize node
00085         ros::NodeHandle n_;
00086                 
00087         // declaration of topics to publish
00088         ros::Publisher topicPub_JointState_;
00089         ros::Publisher topicPub_ControllerState_;
00090         ros::Publisher topicPub_Diagnostic_;
00091         
00092         // declaration of topics to subscribe, callback is called for new messages arriving
00093         ros::Subscriber topicSub_JointCommand_;
00094         
00095         // declaration of service servers
00096         ros::ServiceServer srvServer_Init_;
00097         ros::ServiceServer srvServer_Stop_;
00098         ros::ServiceServer srvServer_Recover_;
00099         ros::ServiceServer srvServer_SetOperationMode_;
00100         ros::ServiceServer srvServer_SetDefaultVel_;
00101                 
00102         // declaration of service clients
00103         //--
00104 
00105         // action lib server
00106         //actionlib::SimpleActionServer<pr2_controllers_msgs::JointTrajectoryAction> as_;
00107         //std::string action_name_;
00108         //pr2_controllers_msgs::JointTrajectoryFeedback feedback_;
00109         //pr2_controllers_msgs::JointTrajectoryResult result_;
00110         actionlib::SimpleActionServer<control_msgs::FollowJointTrajectoryAction> as_;
00111         std::string action_name_;
00112         control_msgs::FollowJointTrajectoryFeedback feedback_;
00113         control_msgs::FollowJointTrajectoryResult result_;
00114         
00115         // global variables
00116         ElmoCtrl * CamAxis_;
00117         ElmoCtrlParams* CamAxisParams_;
00118         
00119         std::string CanDevice_;
00120         std::string CanIniFile_;
00121         int CanBaudrate_;
00122         int HomingDir_;
00123         int HomingDigIn_;
00124         double MaxVel_;
00125         int ModID_;
00126         std::string operationMode_;
00127         double LowerLimit_;
00128         double UpperLimit_; 
00129         double Offset_;
00130         int MotorDirection_;
00131         int EnoderIncrementsPerRevMot_;
00132         double GearRatio_;
00133         
00134         std::string JointName_;
00135         bool isInitialized_;
00136         bool isError_;
00137         bool finished_;
00138         double ActualPos_;
00139         double ActualVel_;
00140         double GoalPos_;
00141         trajectory_msgs::JointTrajectory traj_;
00142         trajectory_msgs::JointTrajectoryPoint traj_point_;
00143         unsigned int traj_point_nr_;
00144 
00145         // Constructor
00146         NodeClass(std::string name):
00147                 as_(n_, name, boost::bind(&NodeClass::executeCB, this, _1)),
00148                 action_name_(name)
00149         {
00150                 n_ = ros::NodeHandle("~");
00151         
00152                 isInitialized_ = false;
00153                 isError_ = false;
00154                 ActualPos_=0.0;
00155                 ActualVel_=0.0;
00156 
00157                 CamAxis_ = new ElmoCtrl();
00158                 CamAxisParams_ = new ElmoCtrlParams();
00159 
00160                 // implementation of topics to publish
00161                 topicPub_JointState_ = n_.advertise<sensor_msgs::JointState>("/joint_states", 1);
00162                 topicPub_ControllerState_ = n_.advertise<pr2_controllers_msgs::JointTrajectoryControllerState>("state", 1);
00163                 topicPub_Diagnostic_ = n_.advertise<diagnostic_msgs::DiagnosticArray>("/diagnostics", 1);
00164 
00165 
00166                 // implementation of topics to subscribe
00167                 
00168                 // implementation of service servers
00169                 srvServer_Init_ = n_.advertiseService("init", &NodeClass::srvCallback_Init, this);
00170                 srvServer_Stop_ = n_.advertiseService("stop", &NodeClass::srvCallback_Stop, this);
00171                 srvServer_Recover_ = n_.advertiseService("recover", &NodeClass::srvCallback_Recover, this);
00172                 srvServer_SetOperationMode_ = n_.advertiseService("set_operation_mode", &NodeClass::srvCallback_SetOperationMode, this);
00173                 srvServer_SetDefaultVel_ = n_.advertiseService("set_default_vel", &NodeClass::srvCallback_SetDefaultVel, this);
00174                 
00175                 // implementation of service clients
00176                 //--
00177 
00178                 // read parameters from parameter server
00179                 if(!n_.hasParam("EnoderIncrementsPerRevMot")) ROS_WARN("cob_head_axis: couldn't find parameter EnoderIncrementsPerRevMot, check if ALL parameters have been set correctly");
00180 
00181                 n_.param<std::string>("CanDevice", CanDevice_, "PCAN");
00182                 n_.param<int>("CanBaudrate", CanBaudrate_, 500);
00183                 n_.param<int>("HomingDir", HomingDir_, 1);
00184                 n_.param<int>("HomingDigIn", HomingDigIn_, 11);
00185                 n_.param<int>("ModId",ModID_, 17);
00186                 n_.param<std::string>("JointName",JointName_, "head_axis_joint");
00187                 n_.param<std::string>("CanIniFile",CanIniFile_, "/");
00188                 n_.param<std::string>("operation_mode",operationMode_, "position");
00189                 n_.param<int>("MotorDirection",MotorDirection_, 1);
00190                 n_.param<double>("GearRatio",GearRatio_, 62.5);
00191                 n_.param<int>("EnoderIncrementsPerRevMot",EnoderIncrementsPerRevMot_, 4096);
00192                 
00193                 ROS_INFO("CanDevice=%s, CanBaudrate=%d, ModID=%d, HomingDigIn=%d",CanDevice_.c_str(),CanBaudrate_,ModID_,HomingDigIn_);
00194                 
00195                 
00196                 // load parameter server string for robot/model
00197                 std::string param_name = "/robot_description";
00198                 std::string full_param_name;
00199                 std::string xml_string;
00200                 n_.searchParam(param_name,full_param_name);
00201                 n_.getParam(full_param_name.c_str(),xml_string);
00202                 ROS_INFO("full_param_name=%s",full_param_name.c_str());
00203                 if (xml_string.size()==0)
00204                 {
00205                         ROS_ERROR("Unable to load robot model from param server robot_description\n");
00206                         exit(2);
00207                 }
00208                 ROS_DEBUG("%s content\n%s", full_param_name.c_str(), xml_string.c_str());
00209                 
00210                 // extract limits and velocitys from urdf model
00211                 urdf::Model model;
00212                 if (!model.initString(xml_string))
00213                 {
00214                         ROS_ERROR("Failed to parse urdf file");
00215                         exit(2);
00216                 }
00217                 ROS_INFO("Successfully parsed urdf file");
00218 
00219                 // get LowerLimits out of urdf model
00220                 LowerLimit_ = model.getJoint(JointName_.c_str())->limits->lower;
00221                         //std::cout << "LowerLimits[" << JointNames[i].c_str() << "] = " << LowerLimits[i] << std::endl;
00222                 CamAxisParams_->SetLowerLimit(LowerLimit_);
00223 
00224                 // get UpperLimits out of urdf model
00225                 UpperLimit_ = model.getJoint(JointName_.c_str())->limits->upper;
00226                         //std::cout << "LowerLimits[" << JointNames[i].c_str() << "] = " << LowerLimits[i] << std::endl;
00227                 CamAxisParams_->SetUpperLimit(UpperLimit_);
00228 
00229                 // get Offset out of urdf model
00230                 Offset_ = model.getJoint(JointName_.c_str())->calibration->rising.get()[0];
00231                         //std::cout << "Offset[" << JointNames[i].c_str() << "] = " << Offsets[i] << std::endl;
00232                 CamAxisParams_->SetAngleOffset(Offset_);
00233                 
00234                 // get velocitiy out of urdf model
00235                 MaxVel_ = model.getJoint(JointName_.c_str())->limits->velocity;
00236                 ROS_INFO("Successfully read limits from urdf");
00237 
00238 
00239                 //initializing and homing of camera_axis                
00240                 CamAxisParams_->SetCanIniFile(CanIniFile_);
00241                 CamAxisParams_->SetHomingDir(HomingDir_);
00242                 CamAxisParams_->SetHomingDigIn(HomingDigIn_);
00243                 CamAxisParams_->SetMaxVel(MaxVel_);
00244                 CamAxisParams_->SetGearRatio(GearRatio_);
00245                 CamAxisParams_->SetMotorDirection(MotorDirection_);
00246                 CamAxisParams_->SetEncoderIncrements(EnoderIncrementsPerRevMot_);
00247                 
00248                 
00249                 
00250 
00251                 CamAxisParams_->Init(CanDevice_, CanBaudrate_, ModID_);
00252                 
00253 
00254         }
00255         
00256         // Destructor
00257         ~NodeClass() 
00258         {
00259                 delete CamAxis_;
00260         }
00261 
00262         //void executeCB(const pr2_controllers_msgs::JointTrajectoryGoalConstPtr &goal) {
00263         void executeCB(const control_msgs::FollowJointTrajectoryGoalConstPtr &goal) {   
00264                 if(isInitialized_) {    
00265                         ROS_INFO("Received new goal trajectory with %d points",goal->trajectory.points.size());
00266                         // saving goal into local variables
00267                         traj_ = goal->trajectory;
00268                         traj_point_nr_ = 0;
00269                         traj_point_ = traj_.points[traj_point_nr_];
00270                         GoalPos_ = traj_point_.positions[0];
00271                         finished_ = false;
00272                         
00273                         // stoping axis to prepare for new trajectory
00274                         CamAxis_->Stop();
00275 
00276                         // check that preempt has not been requested by the client
00277                         if (as_.isPreemptRequested())
00278                         {
00279                                 ROS_INFO("%s: Preempted", action_name_.c_str());
00280                                 // set the action state to preempted
00281                                 as_.setPreempted();
00282                         }
00283                         
00284                         usleep(2000000); // needed sleep until drive starts to change status from idle to moving
00285                         
00286                         while (not finished_)
00287                         {
00288                                 if (as_.isNewGoalAvailable())
00289                                 {
00290                                         ROS_WARN("%s: Aborted", action_name_.c_str());
00291                                         as_.setAborted();
00292                                         return;
00293                                 }
00294                                 usleep(10000);
00295                                 //feedback_ = 
00296                                 //as_.send feedback_
00297                         }
00298 
00299                         // set the action state to succeed                      
00300                         //result_.result.data = "executing trajectory";
00301                         ROS_INFO("%s: Succeeded", action_name_.c_str());
00302                         // set the action state to succeeded
00303                         as_.setSucceeded(result_);
00304                 
00305                 } else {
00306                         as_.setAborted();
00307                         ROS_WARN("Camera_axis not initialized yet!");
00308                 }
00309         }
00310 
00311         // service callback functions
00312         // function will be called when a service is querried
00313         bool srvCallback_Init(cob_srvs::Trigger::Request &req,
00314                                   cob_srvs::Trigger::Response &res )
00315         {
00316                 if (isInitialized_ == false) {
00317                         ROS_INFO("...initializing camera axis...");
00318                         // init axis 
00319                         if (CamAxis_->Init(CamAxisParams_))
00320                         {
00321                                 CamAxis_->setGearPosVelRadS(0.0f, MaxVel_);
00322                                 ROS_INFO("Initializing of camera axis successfully");
00323                                 isInitialized_ = true;
00324                                 res.success.data = true;
00325                                 res.error_message.data = "initializing camera axis successfully";
00326                         }
00327                         else
00328                         {
00329                                 ROS_ERROR("Initializing camera axis not successfully \n");
00330                                 res.success.data = false;
00331                                 res.error_message.data = "initializing camera axis not successfully";
00332                         }
00333                         }
00334                         else
00335                         {
00336                                 ROS_WARN("...camera axis already initialized...");                      
00337                                 res.success.data = true;
00338                                 res.error_message.data = "camera axis already initialized";
00339                 }
00340                 
00341                 return true;
00342         }
00343 
00344         bool srvCallback_Stop(cob_srvs::Trigger::Request &req,
00345                                   cob_srvs::Trigger::Response &res )
00346         {
00347                 ROS_INFO("Stopping camera axis");
00348                 if(isInitialized_)
00349                 {
00350                         // stopping all movements
00351                         if (CamAxis_->Stop()) {
00352                                 ROS_INFO("Stopping camera axis successfully");
00353                                 res.success.data = true;
00354                                 res.error_message.data = "camera axis stopped successfully";
00355                         }
00356                         else {
00357                                 ROS_ERROR("Stopping camera axis not successfully. error");
00358                                 res.success.data = false;
00359                                 res.error_message.data = "stopping camera axis not successfully";
00360                         }
00361                 }
00362                 return true;
00363         }
00364         
00365         bool srvCallback_Recover(cob_srvs::Trigger::Request &req,
00366                                          cob_srvs::Trigger::Response &res )
00367         {
00368                 if (isInitialized_) {
00369                         ROS_INFO("Recovering camera axis");
00370                         
00371                         // stopping all arm movements
00372                         if (CamAxis_->RecoverAfterEmergencyStop()) {
00373                                 ROS_INFO("Recovering camera axis successfully");
00374                                 res.success.data = true;
00375                                 res.error_message.data = "camera axis successfully recovered";
00376                         } else {
00377                                 ROS_ERROR("Recovering camera axis not successfully. error");
00378                                 res.success.data = false;
00379                                 res.error_message.data = "recovering camera axis failed";
00380                         }
00381                 } else {
00382                         ROS_WARN("...camera axis already recovered...");                        
00383                         res.success.data = true;
00384                         res.error_message.data = "camera axis already recovered";
00385                 }
00386 
00387                 return true;
00388         }
00389 
00397         bool srvCallback_SetOperationMode(      cob_srvs::SetOperationMode::Request &req,
00398                                                                                 cob_srvs::SetOperationMode::Response &res )
00399         {
00400                 ROS_INFO("Set operation mode to [%s]", req.operation_mode.data.c_str());
00401                 n_.setParam("operation_mode", req.operation_mode.data.c_str());
00402                 res.success.data = true; // 0 = true, else = false
00403                 return true;
00404         }
00405 
00413         bool srvCallback_SetDefaultVel( cob_srvs::SetDefaultVel::Request &req,
00414                                                                         cob_srvs::SetDefaultVel::Response &res )
00415         {
00416                 ROS_INFO("Set default velocity to [%f]", req.default_vel);
00417                 MaxVel_ = req.default_vel;
00418                 CamAxisParams_->SetMaxVel(MaxVel_);
00419                 CamAxis_->setMaxVelocity(MaxVel_);
00420                 res.success.data = true; // 0 = true, else = false
00421                 return true;
00422         }
00423 
00424         // other function declarations
00425         void updateCommands()
00426                 {
00427                         if (isInitialized_ == true)
00428                         {
00429                                 if (operationMode_ == "position")
00430                                 {
00431                                         ROS_DEBUG("moving head_axis in position mode");
00432 
00433                                         if (fabs(ActualVel_) < 0.02)
00434                                         {
00435                                                 //feedback_.isMoving = false;
00436                                 
00437                                                 ROS_DEBUG("next point is %d from %d",traj_point_nr_,traj_.points.size());
00438                                                 
00439                                                 if (traj_point_nr_ < traj_.points.size())
00440                                                 {
00441                                                         // if axis is not moving and not reached last point of trajectory, then send new target point
00442                                                         ROS_INFO("...moving to trajectory point[%d], %f",traj_point_nr_,traj_.points[traj_point_nr_].positions[0]);
00443                                                         traj_point_ = traj_.points[traj_point_nr_];
00444                                                         CamAxis_->setGearPosVelRadS(traj_point_.positions[0], MaxVel_);
00445                                                         usleep(900000);
00446                                                         CamAxis_->m_Joint->requestPosVel();
00447                                                         traj_point_nr_++;
00448                                                         //feedback_.isMoving = true;
00449                                                         //feedback_.pointNr = traj_point_nr;
00450                                                         //as_.publishFeedback(feedback_);
00451                                                 }
00452                                                 else if ( fabs( fabs(ActualPos_) - fabs(GoalPos_) ) < 0.5*M_PI/180.0 && !finished_ )
00453                                                 {
00454                                                         ROS_DEBUG("...reached end of trajectory");
00455                                                         finished_ = true;
00456                                                 }
00457                                                 else
00458                                                 {
00459                                                         //do nothing until GoalPos_ is reached
00460                                                 }
00461                                         }
00462                                         else
00463                                         {
00464                                                 ROS_INFO("...axis still moving to point[%d]",traj_point_nr_);
00465                                         }
00466                                 }
00467                                 else if (operationMode_ == "velocity")
00468                                 {
00469                                         ROS_WARN("Moving in velocity mode currently disabled");
00470                                 }
00471                                 else
00472                                 {
00473                                         ROS_ERROR("axis neither in position nor in velocity mode. OperationMode = [%s]", operationMode_.c_str());
00474                                 }
00475                         }
00476                         else
00477                         {
00478                                 ROS_DEBUG("axis not initialized");
00479                         }
00480                 }
00481         
00482         void publishJointState()
00483         {
00484                 if (isInitialized_ == true) {                   
00485                         // create message
00486                         int DOF = 1;
00487 
00488                         CamAxis_->evalCanBuffer();
00489                         CamAxis_->getGearPosVelRadS(&ActualPos_,&ActualVel_);
00490                         CamAxis_->m_Joint->requestPosVel();
00491 
00492                         // really bad hack
00493                         ActualPos_ = HomingDir_ * ActualPos_;
00494                         ActualVel_ = HomingDir_ * ActualVel_;
00495 
00496                         sensor_msgs::JointState msg;
00497                         msg.header.stamp = ros::Time::now();
00498                         msg.name.resize(DOF);
00499                         msg.position.resize(DOF);
00500                         msg.velocity.resize(DOF);
00501                         msg.effort.resize(DOF);
00502                         
00503                         msg.name[0] = JointName_;
00504                         msg.position[0] = ActualPos_;
00505                         msg.velocity[0] = ActualVel_;
00506                         msg.effort[0] = 0.0;
00507 
00508 
00509                         //std::cout << "Joint " << msg.name[0] <<": pos="<<  msg.position[0] << " vel=" << msg.velocity[0] << std::endl;
00510                                 
00511                         // publish message
00512                         topicPub_JointState_.publish(msg);
00513 
00514                         // publish controller state message
00515                         pr2_controllers_msgs::JointTrajectoryControllerState controllermsg;
00516                         controllermsg.header = msg.header;
00517                         controllermsg.joint_names.resize(DOF);
00518                         controllermsg.desired.positions.resize(DOF);
00519                         controllermsg.desired.velocities.resize(DOF);
00520                         controllermsg.actual.positions.resize(DOF);
00521                         controllermsg.actual.velocities.resize(DOF);
00522                         controllermsg.error.positions.resize(DOF);
00523                         controllermsg.error.velocities.resize(DOF);
00524                         
00525                         controllermsg.joint_names = msg.name;
00526                         controllermsg.desired.positions = msg.position;
00527                         controllermsg.desired.velocities = msg.velocity;
00528                         controllermsg.actual.positions = msg.position;
00529                         controllermsg.actual.velocities = msg.velocity;
00530                         // error, calculated out of desired and actual values
00531                         for (int i = 0; i<DOF; i++ )
00532                         {
00533                                 controllermsg.error.positions[i] = controllermsg.desired.positions[i] - controllermsg.actual.positions[i];
00534                                 controllermsg.error.velocities[i] = controllermsg.desired.velocities[i] - controllermsg.actual.velocities[i];
00535                         }
00536                         topicPub_ControllerState_.publish(controllermsg);
00537                 }
00538                 // publishing diagnotic messages
00539             diagnostic_msgs::DiagnosticArray diagnostics;
00540             diagnostics.status.resize(1);
00541             // set data to diagnostics
00542             if(isError_)
00543             {
00544               diagnostics.status[0].level = 2;
00545               diagnostics.status[0].name = "head_axis";
00546               diagnostics.status[0].message = "one or more drives are in Error mode";
00547             }
00548             else
00549             {
00550               if (isInitialized_)
00551               {
00552                 diagnostics.status[0].level = 0;
00553                 diagnostics.status[0].name = n_.getNamespace();
00554                 diagnostics.status[0].message = "head axis initialized and running";
00555               }
00556               else
00557               {
00558                 diagnostics.status[0].level = 1;
00559                 diagnostics.status[0].name = n_.getNamespace();
00560                 diagnostics.status[0].message = "head axis not initialized";
00561               }
00562             }
00563             // publish diagnostic message
00564             topicPub_Diagnostic_.publish(diagnostics);
00565         }
00566 
00567 }; //NodeClass
00568 
00569 
00570 //#######################
00571 //#### main programm ####
00572 int main(int argc, char** argv)
00573 {
00574         // initialize ROS, spezify name of node
00575         ros::init(argc, argv, "cob_camera_axis");
00576         
00577         // create nodeClass
00578         //NodeClass nodeClass(ros::this_node::getName() + "/joint_trajectory_action");
00579         NodeClass nodeClass(ros::this_node::getName() + "/follow_joint_trajectory");
00580  
00581         // main loop
00582         ros::Rate loop_rate(10); // Hz
00583         while(nodeClass.n_.ok()) {
00584           
00585                 // publish JointState
00586                 nodeClass.publishJointState();
00587 
00588                 // update commands
00589                 nodeClass.updateCommands();
00590 
00591                 // sleep and waiting for messages, callbacks 
00592                 ros::spinOnce();
00593                 loop_rate.sleep();
00594         }
00595         
00596         return 0;
00597 }
00598 
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines


cob_head_axis
Author(s): Ulrich Reiser
autogenerated on Fri Mar 1 2013 17:48:28