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 #include <unistd.h>
00079 
00080 //####################
00081 //#### node class ####
00082 class NodeClass
00083 {
00084         //
00085         public:
00086         // create a handle for this node, initialize node
00087         ros::NodeHandle n_;
00088                 
00089         // declaration of topics to publish
00090         ros::Publisher topicPub_JointState_;
00091         ros::Publisher topicPub_ControllerState_;
00092         ros::Publisher topicPub_Diagnostic_;
00093         
00094         // declaration of topics to subscribe, callback is called for new messages arriving
00095         ros::Subscriber topicSub_JointCommand_;
00096         
00097         // declaration of service servers
00098         ros::ServiceServer srvServer_Init_;
00099         ros::ServiceServer srvServer_Stop_;
00100         ros::ServiceServer srvServer_Recover_;
00101         ros::ServiceServer srvServer_SetOperationMode_;
00102         ros::ServiceServer srvServer_SetDefaultVel_;
00103                 
00104         // declaration of service clients
00105         //--
00106 
00107         // action lib server
00108         //actionlib::SimpleActionServer<pr2_controllers_msgs::JointTrajectoryAction> as_;
00109         //std::string action_name_;
00110         //pr2_controllers_msgs::JointTrajectoryFeedback feedback_;
00111         //pr2_controllers_msgs::JointTrajectoryResult result_;
00112         actionlib::SimpleActionServer<control_msgs::FollowJointTrajectoryAction> as_;
00113         std::string action_name_;
00114         control_msgs::FollowJointTrajectoryFeedback feedback_;
00115         control_msgs::FollowJointTrajectoryResult result_;
00116         
00117         // global variables
00118         ElmoCtrl * CamAxis_;
00119         ElmoCtrlParams* CamAxisParams_;
00120         
00121         std::string CanDevice_;
00122         std::string CanIniFile_;
00123         int CanBaudrate_;
00124         int HomingDir_;
00125         int HomingDigIn_;
00126         double MaxVel_;
00127         int ModID_;
00128         std::string operationMode_;
00129         double LowerLimit_;
00130         double UpperLimit_; 
00131         double Offset_;
00132         int MotorDirection_;
00133         int EnoderIncrementsPerRevMot_;
00134         double GearRatio_;
00135         
00136         std::string JointName_;
00137         bool isInitialized_;
00138         bool isError_;
00139         bool finished_;
00140         double ActualPos_;
00141         double ActualVel_;
00142         double GoalPos_;
00143         trajectory_msgs::JointTrajectory traj_;
00144         trajectory_msgs::JointTrajectoryPoint traj_point_;
00145         unsigned int traj_point_nr_;
00146 
00147         // Constructor
00148         NodeClass(std::string name):
00149                 as_(n_, name, boost::bind(&NodeClass::executeCB, this, _1)),
00150                 action_name_(name)
00151         {
00152                 n_ = ros::NodeHandle("~");
00153         
00154                 isInitialized_ = false;
00155                 isError_ = false;
00156                 ActualPos_=0.0;
00157                 ActualVel_=0.0;
00158 
00159                 CamAxis_ = new ElmoCtrl();
00160                 CamAxisParams_ = new ElmoCtrlParams();
00161 
00162                 // implementation of topics to publish
00163                 topicPub_JointState_ = n_.advertise<sensor_msgs::JointState>("/joint_states", 1);
00164                 topicPub_ControllerState_ = n_.advertise<pr2_controllers_msgs::JointTrajectoryControllerState>("state", 1);
00165                 topicPub_Diagnostic_ = n_.advertise<diagnostic_msgs::DiagnosticArray>("/diagnostics", 1);
00166 
00167 
00168                 // implementation of topics to subscribe
00169                 
00170                 // implementation of service servers
00171                 srvServer_Init_ = n_.advertiseService("init", &NodeClass::srvCallback_Init, this);
00172                 srvServer_Stop_ = n_.advertiseService("stop", &NodeClass::srvCallback_Stop, this);
00173                 srvServer_Recover_ = n_.advertiseService("recover", &NodeClass::srvCallback_Recover, this);
00174                 srvServer_SetOperationMode_ = n_.advertiseService("set_operation_mode", &NodeClass::srvCallback_SetOperationMode, this);
00175                 srvServer_SetDefaultVel_ = n_.advertiseService("set_default_vel", &NodeClass::srvCallback_SetDefaultVel, this);
00176                 
00177                 // implementation of service clients
00178                 //--
00179 
00180                 // read parameters from parameter server
00181                 if(!n_.hasParam("EnoderIncrementsPerRevMot")) ROS_WARN("cob_head_axis: couldn't find parameter EnoderIncrementsPerRevMot, check if ALL parameters have been set correctly");
00182 
00183                 n_.param<std::string>("CanDevice", CanDevice_, "PCAN");
00184                 n_.param<int>("CanBaudrate", CanBaudrate_, 500);
00185                 n_.param<int>("HomingDir", HomingDir_, 1);
00186                 n_.param<int>("HomingDigIn", HomingDigIn_, 11);
00187                 n_.param<int>("ModId",ModID_, 17);
00188                 n_.param<std::string>("JointName",JointName_, "head_axis_joint");
00189                 n_.param<std::string>("CanIniFile",CanIniFile_, "/");
00190                 n_.param<std::string>("operation_mode",operationMode_, "position");
00191                 n_.param<int>("MotorDirection",MotorDirection_, 1);
00192                 n_.param<double>("GearRatio",GearRatio_, 62.5);
00193                 n_.param<int>("EnoderIncrementsPerRevMot",EnoderIncrementsPerRevMot_, 4096);
00194                 
00195                 ROS_INFO("CanDevice=%s, CanBaudrate=%d, ModID=%d, HomingDigIn=%d",CanDevice_.c_str(),CanBaudrate_,ModID_,HomingDigIn_);
00196                 
00197                 
00198                 // load parameter server string for robot/model
00199                 std::string param_name = "/robot_description";
00200                 std::string full_param_name;
00201                 std::string xml_string;
00202                 n_.searchParam(param_name,full_param_name);
00203                 n_.getParam(full_param_name.c_str(),xml_string);
00204                 ROS_INFO("full_param_name=%s",full_param_name.c_str());
00205                 if (xml_string.size()==0)
00206                 {
00207                         ROS_ERROR("Unable to load robot model from param server robot_description\n");
00208                         exit(2);
00209                 }
00210                 ROS_DEBUG("%s content\n%s", full_param_name.c_str(), xml_string.c_str());
00211                 
00212                 // extract limits and velocitys from urdf model
00213                 urdf::Model model;
00214                 if (!model.initString(xml_string))
00215                 {
00216                         ROS_ERROR("Failed to parse urdf file");
00217                         exit(2);
00218                 }
00219                 ROS_INFO("Successfully parsed urdf file");
00220 
00221                 // get LowerLimits out of urdf model
00222                 LowerLimit_ = model.getJoint(JointName_.c_str())->limits->lower;
00223                         //std::cout << "LowerLimits[" << JointNames[i].c_str() << "] = " << LowerLimits[i] << std::endl;
00224                 CamAxisParams_->SetLowerLimit(LowerLimit_);
00225 
00226                 // get UpperLimits out of urdf model
00227                 UpperLimit_ = model.getJoint(JointName_.c_str())->limits->upper;
00228                         //std::cout << "LowerLimits[" << JointNames[i].c_str() << "] = " << LowerLimits[i] << std::endl;
00229                 CamAxisParams_->SetUpperLimit(UpperLimit_);
00230 
00231                 // get Offset out of urdf model
00232                 Offset_ = model.getJoint(JointName_.c_str())->calibration->rising.get()[0];
00233                         //std::cout << "Offset[" << JointNames[i].c_str() << "] = " << Offsets[i] << std::endl;
00234                 CamAxisParams_->SetAngleOffset(Offset_);
00235                 
00236                 // get velocitiy out of urdf model
00237                 MaxVel_ = model.getJoint(JointName_.c_str())->limits->velocity;
00238                 ROS_INFO("Successfully read limits from urdf");
00239 
00240 
00241                 //initializing and homing of camera_axis                
00242                 CamAxisParams_->SetCanIniFile(CanIniFile_);
00243                 CamAxisParams_->SetHomingDir(HomingDir_);
00244                 CamAxisParams_->SetHomingDigIn(HomingDigIn_);
00245                 CamAxisParams_->SetMaxVel(MaxVel_);
00246                 CamAxisParams_->SetGearRatio(GearRatio_);
00247                 CamAxisParams_->SetMotorDirection(MotorDirection_);
00248                 CamAxisParams_->SetEncoderIncrements(EnoderIncrementsPerRevMot_);
00249                 
00250                 
00251                 
00252 
00253                 CamAxisParams_->Init(CanDevice_, CanBaudrate_, ModID_);
00254                 
00255 
00256         }
00257         
00258         // Destructor
00259         ~NodeClass() 
00260         {
00261                 delete CamAxis_;
00262         }
00263 
00264         //void executeCB(const pr2_controllers_msgs::JointTrajectoryGoalConstPtr &goal) {
00265         void executeCB(const control_msgs::FollowJointTrajectoryGoalConstPtr &goal) {   
00266                 if(isInitialized_) {    
00267                         ROS_INFO("Received new goal trajectory with %d points",goal->trajectory.points.size());
00268                         // saving goal into local variables
00269                         traj_ = goal->trajectory;
00270                         traj_point_nr_ = 0;
00271                         traj_point_ = traj_.points[traj_point_nr_];
00272                         GoalPos_ = traj_point_.positions[0];
00273                         finished_ = false;
00274                         
00275                         // stoping axis to prepare for new trajectory
00276                         CamAxis_->Stop();
00277 
00278                         // check that preempt has not been requested by the client
00279                         if (as_.isPreemptRequested())
00280                         {
00281                                 ROS_INFO("%s: Preempted", action_name_.c_str());
00282                                 // set the action state to preempted
00283                                 as_.setPreempted();
00284                         }
00285                         
00286                         usleep(2000000); // needed sleep until drive starts to change status from idle to moving
00287                         
00288                         while (not finished_)
00289                         {
00290                                 if (as_.isNewGoalAvailable())
00291                                 {
00292                                         ROS_WARN("%s: Aborted", action_name_.c_str());
00293                                         as_.setAborted();
00294                                         return;
00295                                 }
00296                                 usleep(10000);
00297                                 //feedback_ = 
00298                                 //as_.send feedback_
00299                         }
00300 
00301                         // set the action state to succeed                      
00302                         //result_.result.data = "executing trajectory";
00303                         ROS_INFO("%s: Succeeded", action_name_.c_str());
00304                         // set the action state to succeeded
00305                         as_.setSucceeded(result_);
00306                 
00307                 } else {
00308                         as_.setAborted();
00309                         ROS_WARN("Camera_axis not initialized yet!");
00310                 }
00311         }
00312 
00313         // service callback functions
00314         // function will be called when a service is querried
00315         bool srvCallback_Init(cob_srvs::Trigger::Request &req,
00316                                   cob_srvs::Trigger::Response &res )
00317         {
00318                 if (isInitialized_ == false) {
00319                         ROS_INFO("...initializing camera axis...");
00320                         // init axis 
00321                         if (CamAxis_->Init(CamAxisParams_))
00322                         {
00323                                 CamAxis_->setGearPosVelRadS(0.0f, MaxVel_);
00324                                 ROS_INFO("Initializing of camera axis successfully");
00325                                 isInitialized_ = true;
00326                                 res.success.data = true;
00327                                 res.error_message.data = "initializing camera axis successfully";
00328                         }
00329                         else
00330                         {
00331                                 ROS_ERROR("Initializing camera axis not successfully \n");
00332                                 res.success.data = false;
00333                                 res.error_message.data = "initializing camera axis not successfully";
00334                         }
00335                         }
00336                         else
00337                         {
00338                                 ROS_WARN("...camera axis already initialized...");                      
00339                                 res.success.data = true;
00340                                 res.error_message.data = "camera axis already initialized";
00341                 }
00342                 
00343                 return true;
00344         }
00345 
00346         bool srvCallback_Stop(cob_srvs::Trigger::Request &req,
00347                                   cob_srvs::Trigger::Response &res )
00348         {
00349                 ROS_INFO("Stopping camera axis");
00350                 if(isInitialized_)
00351                 {
00352                         // stopping all movements
00353                         if (CamAxis_->Stop()) {
00354                                 ROS_INFO("Stopping camera axis successfully");
00355                                 res.success.data = true;
00356                                 res.error_message.data = "camera axis stopped successfully";
00357                         }
00358                         else {
00359                                 ROS_ERROR("Stopping camera axis not successfully. error");
00360                                 res.success.data = false;
00361                                 res.error_message.data = "stopping camera axis not successfully";
00362                         }
00363                 }
00364                 return true;
00365         }
00366         
00367         bool srvCallback_Recover(cob_srvs::Trigger::Request &req,
00368                                          cob_srvs::Trigger::Response &res )
00369         {
00370                 if (isInitialized_) {
00371                         ROS_INFO("Recovering camera axis");
00372                         
00373                         // stopping all arm movements
00374                         if (CamAxis_->RecoverAfterEmergencyStop()) {
00375                                 ROS_INFO("Recovering camera axis successfully");
00376                                 res.success.data = true;
00377                                 res.error_message.data = "camera axis successfully recovered";
00378                         } else {
00379                                 ROS_ERROR("Recovering camera axis not successfully. error");
00380                                 res.success.data = false;
00381                                 res.error_message.data = "recovering camera axis failed";
00382                         }
00383                 } else {
00384                         ROS_WARN("...camera axis already recovered...");                        
00385                         res.success.data = true;
00386                         res.error_message.data = "camera axis already recovered";
00387                 }
00388 
00389                 return true;
00390         }
00391 
00399         bool srvCallback_SetOperationMode(      cob_srvs::SetOperationMode::Request &req,
00400                                                                                 cob_srvs::SetOperationMode::Response &res )
00401         {
00402                 ROS_INFO("Set operation mode to [%s]", req.operation_mode.data.c_str());
00403                 n_.setParam("operation_mode", req.operation_mode.data.c_str());
00404                 res.success.data = true; // 0 = true, else = false
00405                 return true;
00406         }
00407 
00415         bool srvCallback_SetDefaultVel( cob_srvs::SetDefaultVel::Request &req,
00416                                                                         cob_srvs::SetDefaultVel::Response &res )
00417         {
00418                 ROS_INFO("Set default velocity to [%f]", req.default_vel);
00419                 MaxVel_ = req.default_vel;
00420                 CamAxisParams_->SetMaxVel(MaxVel_);
00421                 CamAxis_->setMaxVelocity(MaxVel_);
00422                 res.success.data = true; // 0 = true, else = false
00423                 return true;
00424         }
00425 
00426         // other function declarations
00427         void updateCommands()
00428                 {
00429                         if (isInitialized_ == true)
00430                         {
00431                                 if (operationMode_ == "position")
00432                                 {
00433                                         ROS_DEBUG("moving head_axis in position mode");
00434 
00435                                         if (fabs(ActualVel_) < 0.02)
00436                                         {
00437                                                 //feedback_.isMoving = false;
00438                                 
00439                                                 ROS_DEBUG("next point is %d from %d",traj_point_nr_,traj_.points.size());
00440                                                 
00441                                                 if (traj_point_nr_ < traj_.points.size())
00442                                                 {
00443                                                         // if axis is not moving and not reached last point of trajectory, then send new target point
00444                                                         ROS_INFO("...moving to trajectory point[%d], %f",traj_point_nr_,traj_.points[traj_point_nr_].positions[0]);
00445                                                         traj_point_ = traj_.points[traj_point_nr_];
00446                                                         CamAxis_->setGearPosVelRadS(traj_point_.positions[0], MaxVel_);
00447                                                         usleep(900000);
00448                                                         CamAxis_->m_Joint->requestPosVel();
00449                                                         traj_point_nr_++;
00450                                                         //feedback_.isMoving = true;
00451                                                         //feedback_.pointNr = traj_point_nr;
00452                                                         //as_.publishFeedback(feedback_);
00453                                                 }
00454                                                 else if ( fabs( fabs(ActualPos_) - fabs(GoalPos_) ) < 0.5*M_PI/180.0 && !finished_ )
00455                                                 {
00456                                                         ROS_DEBUG("...reached end of trajectory");
00457                                                         finished_ = true;
00458                                                 }
00459                                                 else
00460                                                 {
00461                                                         //do nothing until GoalPos_ is reached
00462                                                 }
00463                                         }
00464                                         else
00465                                         {
00466                                                 ROS_DEBUG("...axis still moving to point[%d]",traj_point_nr_);
00467                                         }
00468                                 }
00469                                 else if (operationMode_ == "velocity")
00470                                 {
00471                                         ROS_WARN("Moving in velocity mode currently disabled");
00472                                 }
00473                                 else
00474                                 {
00475                                         ROS_ERROR("axis neither in position nor in velocity mode. OperationMode = [%s]", operationMode_.c_str());
00476                                 }
00477                         }
00478                         else
00479                         {
00480                                 ROS_DEBUG("axis not initialized");
00481                         }
00482                 }
00483         
00484         void publishJointState()
00485         {
00486 
00487                 if (isInitialized_ == true) {
00488                         isError_ = CamAxis_->isError();
00489 
00490                         // create message
00491                         int DOF = 1;
00492 
00493                         CamAxis_->evalCanBuffer();
00494                         CamAxis_->getGearPosVelRadS(&ActualPos_,&ActualVel_);
00495                         CamAxis_->m_Joint->requestPosVel();
00496 
00497                         // really bad hack
00498                         ActualPos_ = HomingDir_ * ActualPos_;
00499                         ActualVel_ = HomingDir_ * ActualVel_;
00500 
00501                         sensor_msgs::JointState msg;
00502                         msg.header.stamp = ros::Time::now();
00503                         msg.name.resize(DOF);
00504                         msg.position.resize(DOF);
00505                         msg.velocity.resize(DOF);
00506                         msg.effort.resize(DOF);
00507                         
00508                         msg.name[0] = JointName_;
00509                         msg.position[0] = ActualPos_;
00510                         msg.velocity[0] = ActualVel_;
00511                         msg.effort[0] = 0.0;
00512 
00513 
00514                         //std::cout << "Joint " << msg.name[0] <<": pos="<<  msg.position[0] << " vel=" << msg.velocity[0] << std::endl;
00515                                 
00516                         // publish message
00517                         topicPub_JointState_.publish(msg);
00518 
00519                         // publish controller state message
00520                         pr2_controllers_msgs::JointTrajectoryControllerState controllermsg;
00521                         controllermsg.header = msg.header;
00522                         controllermsg.joint_names.resize(DOF);
00523                         controllermsg.desired.positions.resize(DOF);
00524                         controllermsg.desired.velocities.resize(DOF);
00525                         controllermsg.actual.positions.resize(DOF);
00526                         controllermsg.actual.velocities.resize(DOF);
00527                         controllermsg.error.positions.resize(DOF);
00528                         controllermsg.error.velocities.resize(DOF);
00529                         
00530                         controllermsg.joint_names = msg.name;
00531                         controllermsg.desired.positions = msg.position;
00532                         controllermsg.desired.velocities = msg.velocity;
00533                         controllermsg.actual.positions = msg.position;
00534                         controllermsg.actual.velocities = msg.velocity;
00535                         // error, calculated out of desired and actual values
00536                         for (int i = 0; i<DOF; i++ )
00537                         {
00538                                 controllermsg.error.positions[i] = controllermsg.desired.positions[i] - controllermsg.actual.positions[i];
00539                                 controllermsg.error.velocities[i] = controllermsg.desired.velocities[i] - controllermsg.actual.velocities[i];
00540                         }
00541                         topicPub_ControllerState_.publish(controllermsg);
00542                 }
00543                 // publishing diagnotic messages
00544             diagnostic_msgs::DiagnosticArray diagnostics;
00545             diagnostics.status.resize(1);
00546             // set data to diagnostics
00547             if(isError_)
00548             {
00549               diagnostics.status[0].level = 2;
00550               diagnostics.status[0].name = n_.getNamespace();
00551               diagnostics.status[0].message = "drive is in error mode";
00552             }
00553             else
00554             {
00555               if (isInitialized_)
00556               {
00557                 diagnostics.status[0].level = 0;
00558                 diagnostics.status[0].name = n_.getNamespace();
00559                 diagnostics.status[0].message = "head axis initialized and running";
00560               }
00561               else
00562               {
00563                 diagnostics.status[0].level = 1;
00564                 diagnostics.status[0].name = n_.getNamespace();
00565                 diagnostics.status[0].message = "head axis not initialized";
00566               }
00567             }
00568             // publish diagnostic message
00569             topicPub_Diagnostic_.publish(diagnostics);
00570         }
00571 
00572 }; //NodeClass
00573 
00574 
00575 //#######################
00576 //#### main programm ####
00577 int main(int argc, char** argv)
00578 {
00579         // initialize ROS, spezify name of node
00580         ros::init(argc, argv, "cob_camera_axis");
00581         
00582         // create nodeClass
00583         //NodeClass nodeClass(ros::this_node::getName() + "/joint_trajectory_action");
00584         NodeClass nodeClass(ros::this_node::getName() + "/follow_joint_trajectory");
00585  
00586         // main loop
00587         ros::Rate loop_rate(10); // Hz
00588         while(nodeClass.n_.ok()) {
00589           
00590                 // publish JointState
00591                 nodeClass.publishJointState();
00592 
00593                 // update commands
00594                 nodeClass.updateCommands();
00595 
00596                 // sleep and waiting for messages, callbacks 
00597                 ros::spinOnce();
00598                 loop_rate.sleep();
00599         }
00600         
00601         return 0;
00602 }
00603 


cob_head_axis
Author(s): Ulrich Reiser
autogenerated on Sun Oct 5 2014 23:07:23