GazeboInterface.cpp
Go to the documentation of this file.
00001 #include "r2_gazebo_interface/GazeboInterface.h"
00002 
00003 using namespace gazebo;
00004 
00005 GazeboInterface::GazeboInterface()
00006     : ModelPlugin()
00007     , advancedMode(false)
00008 {
00009 }
00010 
00011 GazeboInterface::~GazeboInterface()
00012 {
00013     // shutdown ros
00014     rosNodePtr->shutdown();
00015 }
00016 
00017 void GazeboInterface::Load(physics::ModelPtr _model, sdf::ElementPtr _sdf)
00018 {
00019     // Store the pointer to the model
00020     modelPtr = _model;
00021 
00022     prevStatesUpdateTime = _model->GetWorld()->GetSimTime();
00023     prevStatusUpdateTime = _model->GetWorld()->GetSimTime();
00024 
00025     // create controller
00026     robotControllerPtr.reset(new RobotController(modelPtr));
00027 
00028     // load parameters
00029     std::string robotNamespace = "";
00030 
00031     if (_sdf->HasElement("robotNamespace"))
00032     {
00033         robotNamespace = _sdf->GetElement("robotNamespace")->Get<std::string>() + "/";
00034     }
00035 
00036     std::string paramsNamespace = "";
00037 
00038     if (_sdf->HasElement("paramsNamespace"))
00039     {
00040         paramsNamespace = _sdf->GetElement("paramsNamespace")->Get<std::string>() + "/";
00041     }
00042 
00043     if (!_sdf->HasElement("jointCommandsTopic"))
00044     {
00045         ROS_FATAL("GazeboInterface plugin missing <jointCommandsTopic>, cannot proceed");
00046         return;
00047     }
00048     else
00049     {
00050         jointCommandsTopic = _sdf->GetElement("jointCommandsTopic")->Get<std::string>();
00051     }
00052 
00053     if (!_sdf->HasElement("jointStatesTopic"))
00054     {
00055         ROS_FATAL("GazeboInterface plugin missing <jointStatesTopic>, cannot proceed");
00056         return;
00057     }
00058     else
00059     {
00060         jointStatesTopic = _sdf->GetElement("jointStatesTopic")->Get<std::string>();
00061     }
00062 
00063     if (!_sdf->HasElement("jointCapabilitiesTopic"))
00064     {
00065         ROS_FATAL("GazeboInterface plugin missing <jointCapabilitiesTopic>, cannot proceed");
00066         return;
00067     }
00068     else
00069     {
00070         jointCapabilitiesTopic = _sdf->GetElement("jointCapabilitiesTopic")->Get<std::string>();
00071     }
00072 
00073     if (!_sdf->HasElement("jointStatesRate"))
00074     {
00075         jointStatesStepTime = 0.;
00076     }
00077     else
00078     {
00079         jointStatesStepTime = 1. / _sdf->GetElement("jointStatesRate")->Get<double>();
00080     }
00081 
00082     if (!_sdf->HasElement("jointCommandRefsTopic"))
00083     {
00084         ROS_FATAL("GazeboInterface plugin missing <jointCommandRefsTopic>, cannot proceed");
00085         return;
00086     }
00087     else
00088     {
00089         jointCommandRefsTopic = _sdf->GetElement("jointCommandRefsTopic")->Get<std::string>();
00090     }
00091 
00092     if (!_sdf->HasElement("advancedMode"))
00093     {
00094         ROS_INFO("GazeboInterface plugin missing <advancedMode>, defaults to false");
00095         advancedMode = false;
00096     }
00097     else
00098     {
00099         std::string strVal = _sdf->GetElement("advancedMode")->Get<std::string>();
00100 
00101         if (strVal == "1")
00102         {
00103             advancedMode = true;
00104         }
00105         else if (strVal == "0")
00106         {
00107             advancedMode = false;
00108         }
00109         else
00110         {
00111             ROS_WARN("GazeboInterface plugin <advancedMode> should be type bool (true/false), defaults to false");
00112             advancedMode = false;
00113         }
00114     }
00115 
00116     ROS_INFO("GazeboInterface plugin advancedMode: %s", advancedMode ? "true" : "false");
00117 
00118     if (advancedMode)
00119     {
00120         if (!_sdf->HasElement("jointControlTopic"))
00121         {
00122             ROS_FATAL("GazeboInterface plugin missing <jointControlTopic>, cannot proceed");
00123             return;
00124         }
00125         else
00126         {
00127             jointControlTopic = _sdf->GetElement("jointControlTopic")->Get<std::string>();
00128         }
00129 
00130         if (!_sdf->HasElement("jointStatusTopic"))
00131         {
00132             ROS_FATAL("GazeboInterface plugin missing <jointStatusTopic>, cannot proceed");
00133             return;
00134         }
00135         else
00136         {
00137             jointStatusTopic = _sdf->GetElement("jointStatusTopic")->Get<std::string>();
00138         }
00139 
00140         if (!_sdf->HasElement("jointStatusRate"))
00141         {
00142             jointStatusStepTime = 0.;
00143         }
00144         else
00145         {
00146             jointStatusStepTime = 1. / _sdf->GetElement("jointStatusRate")->Get<double>();
00147         }
00148     }
00149 
00150     // create ros nodes
00151     rosNodePtr.reset(new ros::NodeHandle(robotNamespace));
00152     paramsNodePtr.reset(new ros::NodeHandle(paramsNamespace));
00153 
00154     // Listen to the update event. This event is broadcast every
00155     // simulation iteration.
00156     updateConnectionPtr = event::Events::ConnectWorldUpdateBegin(boost::bind(&GazeboInterface::update, this));
00157 
00158     ROS_INFO("Gazebo Interface plugin loaded");
00159 }
00160 
00161 void GazeboInterface::traverseParams(XmlRpc::XmlRpcValue param, std::map<std::string, XmlRpc::XmlRpcValue>& valMap, std::string searchKey,
00162                                      std::string ns, std::string name)
00163 {
00164     std::string fullName;
00165 
00166     if (name.empty())
00167     {
00168         fullName = ns;
00169     }
00170     else
00171     {
00172         if (ns.empty())
00173         {
00174             fullName = name;
00175         }
00176         else
00177         {
00178             fullName = ns + "/" + name;
00179         }
00180     }
00181 
00182     if (param.getType() == XmlRpc::XmlRpcValue::TypeStruct)
00183     {
00184         // if it is a struct, recurse
00185         for (XmlRpc::XmlRpcValue::iterator paramIt = param.begin(); paramIt != param.end(); ++paramIt)
00186         {
00187             traverseParams(paramIt->second, valMap, searchKey, fullName, paramIt->first);
00188         }
00189     }
00190     else if (searchKey == "")
00191     {
00192         // if there is no search key, add to map with fully qualified name
00193         valMap.insert(std::make_pair(fullName, param));
00194     }
00195     else if (searchKey == name)
00196     {
00197         // if the searchKey matches the name, add to map omitting searchKey
00198         valMap.insert(std::make_pair(ns, param));
00199     }
00200 
00201     // if searchKey is not matched, the function does nothing
00202 }
00203 
00204 bool GazeboInterface::getDoubleVal(XmlRpc::XmlRpcValue& val, double& doubleVal)
00205 {
00206     if (val.getType() == XmlRpc::XmlRpcValue::TypeInt)
00207     {
00208         doubleVal = (double)(int)val;
00209     }
00210     else if (val.getType() == XmlRpc::XmlRpcValue::TypeDouble)
00211     {
00212         doubleVal = (double)val;
00213     }
00214     else
00215     {
00216         return false;
00217     }
00218 
00219     return true;
00220 }
00221 
00222 void GazeboInterface::Init()
00223 {
00224     ROS_DEBUG("add joints");
00225 
00226     physics::Joint_V model_joints = modelPtr->GetJoints();
00227 
00228     // add all joints to controller
00229     for (physics::Joint_V::iterator j_it = model_joints.begin(); j_it != model_joints.end(); ++j_it)
00230     {
00231         robotControllerPtr->addJoint((*j_it), advancedMode);
00232         ROS_DEBUG("Add %s to controller", (*j_it)->GetName().c_str());
00233     }
00234 
00235     XmlRpc::XmlRpcValue param;
00236 
00237     // set joint dependencies
00238     if (paramsNodePtr->getParam("dependency", param))
00239     {
00240         ROS_DEBUG("set dependencies");
00241         // get dependent children
00242         std::map<std::string, XmlRpc::XmlRpcValue> dependentChildrenMap;
00243         traverseParams(param, dependentChildrenMap, "children");
00244         // get dependent factors
00245         std::map<std::string, XmlRpc::XmlRpcValue> dependentFactorsMap;
00246         traverseParams(param, dependentFactorsMap, "factors");
00247 
00248         // iterate through and add dependencies
00249         for (std::map<std::string, XmlRpc::XmlRpcValue>::iterator depIt = dependentChildrenMap.begin();
00250                 depIt != dependentChildrenMap.end(); ++depIt)
00251         {
00252             XmlRpc::XmlRpcValue factorVal = dependentFactorsMap[depIt->first];
00253 
00254             // value can be a string or an array of strings
00255             if (depIt->second.getType() == XmlRpc::XmlRpcValue::TypeString)
00256             {
00257                 double val;
00258 
00259                 if (getDoubleVal(factorVal, val))
00260                 {
00261                     // child, parent, factor
00262                     ROS_INFO("Add joint dependency: %s, %s, %f", ((std::string)depIt->second).c_str(), depIt->first.c_str(), (double)factorVal);
00263                     robotControllerPtr->addJointDependency((std::string)depIt->second, depIt->first, (double)factorVal);
00264                 }
00265                 else
00266                 {
00267                     ROS_WARN("Add dependency for %s (child) to %s (parent) not completed because factor not an appropriate type (double or int)",
00268                              ((std::string)depIt->second).c_str(), depIt->first.c_str());
00269                 }
00270             }
00271             else if (depIt->second.getType() == XmlRpc::XmlRpcValue::TypeArray)
00272             {
00273                 if (factorVal.getType() == XmlRpc::XmlRpcValue::TypeArray && factorVal.size() == depIt->second.size())
00274                 {
00275                     for (int i = 0; i < depIt->second.size(); ++i)
00276                     {
00277                         if (depIt->second[i].getType() == XmlRpc::XmlRpcValue::TypeString)
00278                         {
00279                             double val;
00280 
00281                             if (getDoubleVal(factorVal[i], val))
00282                             {
00283                                 // child, parent, factor
00284                                 ROS_INFO("Add joint dependency: %s, %s, %f", ((std::string)depIt->second[i]).c_str(), depIt->first.c_str(), val);
00285                                 robotControllerPtr->addJointDependency((std::string)depIt->second[i], depIt->first, val);
00286                             }
00287                             else
00288                             {
00289                                 ROS_WARN("Add dependency for %s (child) to %s (parent) not completed because factor not an appropriate type (double or int)",
00290                                          ((std::string)depIt->second[i]).c_str(), depIt->first.c_str());
00291                             }
00292                         }
00293                         else
00294                         {
00295                             ROS_WARN("A dependency to %s (parent) not added because type is invalid", depIt->first.c_str());
00296                         }
00297                     }
00298                 }
00299                 else
00300                 {
00301                     ROS_WARN("Add dependency to %s (parent) not completed because children/factors array size not the same",
00302                              depIt->first.c_str());
00303                 }
00304             }
00305         }
00306     }
00307 
00308     // get initial positions
00309     if (paramsNodePtr->getParam("initial_position", param))
00310     {
00311         ROS_DEBUG("set initial positions");
00312         bool radians = true;
00313 
00314         if (param.hasMember("radians") && param["radians"].getType() == XmlRpc::XmlRpcValue::TypeBoolean)
00315         {
00316             radians = (bool)param["radians"];
00317             ROS_DEBUG("radians: %d", radians);
00318         }
00319         else
00320         {
00321             ROS_WARN("No 'radians' member provided (or invalid type) in 'initial_position'; assumed true");
00322         }
00323 
00324         std::map<std::string, XmlRpc::XmlRpcValue> initialPoseMapParams;
00325 
00326         if (param.hasMember("position"))
00327         {
00328             traverseParams(param["position"], initialPoseMapParams);
00329 
00330             // get positions
00331             std::map<std::string, double> initialPoseMap;
00332 
00333             for (std::map<std::string, XmlRpc::XmlRpcValue>::iterator poseIt = initialPoseMapParams.begin();
00334                     poseIt != initialPoseMapParams.end(); ++poseIt)
00335             {
00336                 double val;
00337 
00338                 if (getDoubleVal(poseIt->second, val))
00339                 {
00340                     if (radians == false)
00341                     {
00342                         val = GZ_DTOR(val);
00343                     }
00344 
00345                     if (modelPtr->GetJoint(poseIt->first))
00346                     {
00347                         initialPoseMap.insert(std::make_pair(poseIt->first, val));
00348                         ROS_DEBUG("initial position of %s set to %f", poseIt->first.c_str(), val);
00349                     }
00350                     else
00351                     {
00352                         ROS_INFO("initial position of %s ignored because model doesn't contain joint", poseIt->first.c_str());
00353                     }
00354                 }
00355                 else
00356                 {
00357                     ROS_WARN("Sim initial position value (%s) ignored because it is not a valid type (double or int)", poseIt->first.c_str());
00358                 }
00359             }
00360 
00361             // set positions
00362             robotControllerPtr->setJointPositions(initialPoseMap);
00363         }
00364     }
00365     else
00366     {
00367         ROS_WARN("No 'position' member provided in 'initial_position'");
00368     }
00369 
00370     // get position PIDsmodelPtr->GetWorld()->GetSimTime()
00371     if (paramsNodePtr->getParam("position_pid", param))
00372     {
00373         ROS_DEBUG("set position PIDs");
00374         std::map<std::string, XmlRpc::XmlRpcValue> positionPidMap;
00375         traverseParams(param, positionPidMap);
00376 
00377         // iterate through and add PIDs
00378         for (std::map<std::string, XmlRpc::XmlRpcValue>::iterator pidIt = positionPidMap.begin();
00379                 pidIt != positionPidMap.end(); ++pidIt)
00380         {
00381             if (pidIt->second.getType() == XmlRpc::XmlRpcValue::TypeArray && pidIt->second.size() >= 3)
00382             {
00383                 std::vector<double> gains{0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0}; //RDEV-3542-R2 Sim Working in Gazebo 7.14- setting _cmdMax less than _cmdMin to diable clamping. '-1' by default
00384                 bool valid = true;
00385 
00386                 for (int i = 0; i < pidIt->second.size(); ++ i)
00387                 {
00388                     valid &= getDoubleVal(pidIt->second[i], gains[i]);
00389                 }
00390 
00391                 if (valid)
00392                 {
00393                     ROS_DEBUG("setPosPid: %s, %f, %f, %f, %f, %f, %f, %f", pidIt->first.c_str(), gains[0], gains[1], gains[2], gains[3], gains[4], gains[5], gains[6]);
00394                     robotControllerPtr->setPosPid(pidIt->first, gains[0], gains[1], gains[2], gains[3], gains[4], gains[5], gains[6]);
00395                 }
00396                 else
00397                 {
00398                     ROS_WARN("position PID for %s not set because the values are not valid (double or int)", pidIt->first.c_str());
00399                 }
00400             }
00401             else
00402             {
00403                 ROS_WARN("position PID values for %s not set because it is not an array of size 3 or greater", pidIt->first.c_str());
00404             }
00405         }
00406     }
00407 
00408     // get velocity PIDs
00409     if (paramsNodePtr->getParam("velocity_pid", param))
00410     {
00411         ROS_DEBUG("set velocity PIDs");
00412         std::map<std::string, XmlRpc::XmlRpcValue> velocityPidMap;
00413         traverseParams(param, velocityPidMap);
00414 
00415         // iterate through and add PIDs
00416         for (std::map<std::string, XmlRpc::XmlRpcValue>::iterator pidIt = velocityPidMap.begin();
00417                 pidIt != velocityPidMap.end(); ++pidIt)
00418         {
00419             if (pidIt->second.getType() == XmlRpc::XmlRpcValue::TypeArray && pidIt->second.size() >= 3)
00420             {
00421                 std::vector<double> gains{0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0}; //RDEV-3542 fix for position_pid and revising this PID too
00422                 bool valid = true;
00423 
00424                 for (int i = 0; i < pidIt->second.size(); ++ i)
00425                 {
00426                     valid &= getDoubleVal(pidIt->second[i], gains[i]);
00427                 }
00428 
00429                 if (valid)
00430                 {
00431                     ROS_DEBUG("setVelPid: %s, %f, %f, %f, %f, %f, %f, %f", pidIt->first.c_str(), gains[0], gains[1], gains[2], gains[3], gains[4], gains[5], gains[6]);
00432                     robotControllerPtr->setVelPid(pidIt->first, gains[0], gains[1], gains[2], gains[3], gains[4], gains[5], gains[6]);
00433                 }
00434                 else
00435                 {
00436                     ROS_WARN("velocity PID for %s not set because the values are not valid (double or int)", pidIt->first.c_str());
00437                 }
00438             }
00439             else
00440             {
00441                 ROS_WARN("velocity PID values for %s not set because it is not an array of size 3 or greater", pidIt->first.c_str());
00442             }
00443         }
00444     }
00445 
00446     // if not in advanced mode, tell the controller to hold the joint positions
00447     if (!advancedMode)
00448     {
00449 
00450         physics::Joint_V model_joints2 = modelPtr->GetJoints();
00451 
00452         // add all joints to controller
00453         for (physics::Joint_V::iterator j_it2 = model_joints.begin(); j_it2 != model_joints.end(); ++j_it2)
00454         {
00455             robotControllerPtr->setJointPosTarget((*j_it2)->GetName(), (*j_it2)->GetAngle(0).Radian());
00456         }
00457     }
00458 
00459     // subscribe to ROS joint commands
00460     jointCommandsSub  = rosNodePtr->subscribe(jointCommandsTopic,  5, &GazeboInterface::commandJoints,  this);
00461 
00462     // advertise joint state publishing
00463     jointStatePub = rosNodePtr->advertise<sensor_msgs::JointState>(jointStatesTopic, true);
00464 
00465     // advertise joint capabilities publishing
00466     jointCapabilitiesPub = rosNodePtr->advertise<r2_msgs::JointCapability>(jointCapabilitiesTopic, true);
00467 
00468     // advertise joint command ref publishing
00469     jointCommandRefsPub = rosNodePtr->advertise<r2_msgs::JointCommand>(jointCommandRefsTopic, true);
00470 
00471     if (advancedMode)
00472     {
00473         // subscribe to ROS joint control messages
00474         jointControlSub  = rosNodePtr->subscribe(jointControlTopic,  5, &GazeboInterface::controlJoints,  this);
00475 
00476         // advertise joint status publishing
00477         jointStatusPub = rosNodePtr->advertise<r2_msgs::JointStatusArray>(jointStatusTopic, true);
00478     }
00479 
00480     ROS_INFO("Gazebo Interface plugin initialized");
00481 }
00482 
00483 void GazeboInterface::update()
00484 {
00485     common::Time currTime = modelPtr->GetWorld()->GetSimTime();
00486 
00487     // update joint controller
00488     robotControllerPtr->update();
00489 
00490     //publish joint states
00491     if ((currTime - prevStatesUpdateTime).Double() >= jointStatesStepTime)
00492     {
00493         prevStatesUpdateTime = currTime;
00494         sensor_msgs::JointStatePtr msgPtr(new sensor_msgs::JointState);
00495         r2_msgs::JointCommandPtr refPtr(new r2_msgs::JointCommand);
00496         r2_msgs::JointCapabilityPtr capPtr(new r2_msgs::JointCapability);
00497         msgPtr->header.stamp = ros::Time::now();
00498         refPtr->header.stamp = ros::Time::now();
00499 
00500         // get positions
00501         robotControllerPtr->getJointStates(jointStates);
00502 
00503         // build position message
00504         for (std::map<std::string, RobotController::JointState>::const_iterator stateIt = jointStates.begin(); stateIt != jointStates.end(); ++stateIt)
00505         {
00506             std::string::size_type index = stateIt->first.find("fixed");
00507 
00508             if (index != std::string::npos)
00509             {
00510                 continue;
00511             }
00512 
00513             index = stateIt->first.find("/joint");
00514 
00515             if (index != std::string::npos)
00516             {
00517                 std::string name = stateIt->first;
00518                 msgPtr->name.push_back(name);
00519                 msgPtr->position.push_back(stateIt->second.position);
00520                 msgPtr->velocity.push_back(stateIt->second.velocity);
00521                 msgPtr->effort.push_back(stateIt->second.effort);
00522             }
00523             else
00524             {
00525                 msgPtr->name.push_back(stateIt->first);
00526                 msgPtr->position.push_back(stateIt->second.position);
00527                 msgPtr->velocity.push_back(stateIt->second.velocity);
00528                 msgPtr->effort.push_back(stateIt->second.effort);
00529             }
00530         }
00531 
00532         // get targets
00533         robotControllerPtr->getJointTargets(jointCommandRefs);
00534 
00535         // build targets message
00536         for (std::map<std::string, RobotController::JointState>::const_iterator refsIt = jointCommandRefs.begin(); refsIt != jointCommandRefs.end(); ++refsIt)
00537         {
00538             std::string::size_type index = refsIt->first.find("fixed");
00539 
00540             if (index == std::string::npos)
00541             {
00542                 refPtr->name.push_back(refsIt->first);
00543                 refPtr->desiredPosition.push_back(refsIt->second.position);
00544                 refPtr->desiredPositionVelocityLimit.push_back(refsIt->second.velocity);
00545                 refPtr->feedForwardTorque.push_back(refsIt->second.effort);
00546                 refPtr->proportionalGain.push_back(0);
00547                 refPtr->derivativeGain.push_back(0);
00548                 refPtr->integralGain.push_back(0);
00549                 refPtr->positionLoopTorqueLimit.push_back(0);
00550                 refPtr->positionLoopWindupLimit.push_back(0);
00551                 refPtr->torqueLoopVelocityLimit.push_back(0);
00552             }
00553         }
00554 
00555         // get capabilities
00556         robotControllerPtr->getJointLimits(jointLimits);
00557 
00558         // build capability message
00559         for (std::map<std::string, std::pair<double, double> >::const_iterator limsIt = jointLimits.begin(); limsIt != jointLimits.end(); ++limsIt)
00560         {
00561             std::string::size_type index = limsIt->first.find("fixed");
00562 
00563             if (index == std::string::npos)
00564             {
00565                 capPtr->name.push_back(limsIt->first);
00566                 capPtr->positionLimitMax.push_back(limsIt->second.second);
00567                 capPtr->positionLimitMin.push_back(limsIt->second.first);
00568                 capPtr->velocityLimit.push_back(2.5);
00569                 capPtr->torqueLimit.push_back(1000.);
00570             }
00571         }
00572 
00573         jointCommandRefsPub.publish(refPtr);
00574         jointStatePub.publish(msgPtr);
00575         //jointCommandRefsPub.publish(refPtr);
00576         jointCapabilitiesPub.publish(capPtr);
00577     }
00578 
00579     //publish joint status
00580     if (advancedMode && (currTime - prevStatusUpdateTime).Double() >= jointStatusStepTime)
00581     {
00582         prevStatusUpdateTime = currTime;
00583         robotControllerPtr->publishJointStatuses(jointStatusPub);
00584     }
00585 }
00586 
00587 void GazeboInterface::commandJoints(const sensor_msgs::JointState::ConstPtr& msg)
00588 {
00589     bool setPos = msg->position.size() >= msg->name.size();
00590     bool setVel  = msg->velocity.size() >= msg->name.size();
00591     bool setEffort  = msg->effort.size() >= msg->name.size();
00592 
00593     ROS_DEBUG("GazeboInterface received joint command");
00594 
00595     for (unsigned int i = 0; i < msg->name.size(); ++i)
00596     {
00597         if (setPos)
00598         {
00599             robotControllerPtr->setJointPosTarget(msg->name[i], msg->position[i]);
00600         }
00601         else if (setEffort)
00602         {
00603             robotControllerPtr->setJointEffortTarget(msg->name[i], msg->effort[i]);
00604         }
00605         else if (setVel)
00606         {
00607             robotControllerPtr->setJointVelTarget(msg->name[i], msg->velocity[i]);
00608         }
00609     }
00610 }
00611 
00612 // handle control message
00613 void GazeboInterface::controlJoints(const r2_msgs::JointControl::ConstPtr& msg)
00614 {
00615     ROS_DEBUG("GazeboInterface received joint control");
00616     robotControllerPtr->setJointControl(msg);
00617 }
00618 
00619 GZ_REGISTER_MODEL_PLUGIN(GazeboInterface)


r2_gazebo_interface
Author(s):
autogenerated on Fri Jun 21 2019 20:03:41