Tekkotsu Homepage Demos Overview Downloads Dev. Resources Reference Credits

Go to the documentation of this file.
```00001 #include "Shared/RobotInfo.h"
00002 #if (defined(TGT_HAS_LEGS) && !defined(TGT_IS_AIBO)) || defined(TGT_HAS_HEAD)
00003
00005 #include "Shared/debuget.h"
00006 #include "Shared/fmatSpatial.h"
00007 #include <limits>
00008
00009 using namespace std;
00010
00011 const float IKThreeLink::EPSILON=1e-3f;
00013
00014 bool IKThreeLink::solve(const Point& pEff, const Rotation& oriEff, KinematicJoint& j, const Position& pTgt, float posPri, const Orientation& oriTgt, float oriPri) const {
00015   bool converges[3] = { true, true, true };
00016   KinematicJoint* links[3] = { NULL, NULL, NULL };
00017   float origKnee;
00019
00020   // we're going to do an analytic solution for closest point on target surface.
00021   // find this target point
00022   fmat::Transform Tj = j.getFullT();
00023   fmat::Column<3> pEffBase(Tj*pEff);
00024   fmat::Quaternion qj = j.getWorldQuaternion(); //cout << "WorldQ: " << qj;
00025   Rotation oEffBase = qj*oriEff;
00026   fmat::Column<3> de;
00028
00029   if(links[0]==NULL || posPri<=0) { // no mobile links
00030     // maybe we're conveniently at the solution... check error
00031     std::cerr << "ERROR: IKThreeLink trying to solve without any mobile joints (" << j.outputOffset.get() << ")" << std::endl;
00032     return de.norm()<=EPSILON;
00033   }
00034
00035   origKnee = (links[2]!=NULL) ? links[2]->getQ() : 0;
00036   hasInverseSolution=false;
00037   fmat::Column<3> pTgtV = pEffBase + de;
00038
00039   /*std::cout << "Q prior " << links[0]->getQ();
00041     cout << ' ' << links[1]->getQ();
00043     cout << ' ' << links[2]->getQ();
00044   cout << std::endl;*/
00047       computeThirdLink(*links[2], pTgtV, j, pEff, invertThird, converges[2]);
00048     computeSecondLink(*links[1], pTgtV, j, pEff, false, converges[1]);
00049   }
00051
00052   if(hasInverseSolution && (!converges[0] || !converges[1])) {
00053     // we hit a joint limit, try inverse solution?
00054     //cout << "Hit limit, trying inverse " << inverseKnee << endl;
00055     float defaultKnee = links[2]->getQ();
00058       computeSecondLink(*links[1], pTgtV, j, pEff, false, converges[1]);
00060
00061     /*std::cout << "Inverse Q post " << links[0]->getQ();
00063       cout << ' ' << links[1]->getQ();
00065       cout << ' ' << links[2]->getQ();
00066     cout << std::endl;*/
00067
00068     if((!converges[0] || !converges[1]) && std::abs(origKnee-inverseKnee) > std::abs(origKnee-defaultKnee)) {
00069       // no luck, go back to default if it was closer to current position
00070       //cout << "Still hit limit, going back" << endl;
00073         computeSecondLink(*links[1], pTgtV, j, pEff, false, converges[1]);
00075     }
00076   }
00077
00078   /*std::cout << "Q post " << links[0]->getQ();
00080     cout << ' ' << links[1]->getQ();
00082     cout << ' ' << links[2]->getQ();
00083   cout << std::endl;*/
00084
00085   //check if root link is maxed out
00086   if(!converges[0]) {
00087     //redo child links since their parent was limited, first child is now root
00089       hasInverseSolution=false;
00091         computeSecondLink(*links[2], pTgtV, j, pEff, invertThird, converges[2]);
00092       }
00094       if(hasInverseSolution && !converges[1]) {
00095         float defaultKnee = links[2]->getQ();
00098         if(!converges[1] && std::abs(origKnee-inverseKnee) > std::abs(origKnee-defaultKnee)) {
00099           // no luck, go back to default if it was closer to current position
00102         }
00103       }
00104     }
00105   }
00106
00107   //check again, maybe now middle link is limited
00108   if(!converges[1] && links[2]!=NULL) {
00109     //redo last link
00111   }
00112
00113   de = j.getFullT()*pEff - pTgtV;
00114   return de.norm()<=EPSILON;
00115 }
00116
00117 IKSolver::StepResult_t IKThreeLink::step(const Point& pEff, const Rotation& oriEff, KinematicJoint& j, const Position& pTgt, float pDist, float posPri, const Orientation& oriTgt, float oriDist, float oriPri) const {
00118   // find the closest target point
00119   fmat::Transform Tj = j.getFullT();
00120   fmat::Column<3> pEffBase(Tj*pEff);
00121   fmat::Quaternion qj = j.getWorldQuaternion(); //cout << "WorldQ: " << qj;
00122   Rotation oEffBase = qj*oriEff;
00123   Point de;
00125
00126   StepResult_t res=SUCCESS;
00127   if(de.norm()>pDist) {
00128     de*=pDist/de.norm();
00129     res = PROGRESS;
00130   }
00131   Point pTgtP = pEffBase+de;
00132   if(solve(pEff,oriEff,j,pTgtP,posPri,oriTgt,oriPri))
00133     return res;
00134   return LIMITS;
00135 }
00136
00137 unsigned int IKThreeLink::setLinks(KinematicJoint& eff, KinematicJoint* links[], unsigned int remain) {
00138   if(remain==0)
00139     return 0;
00140   if(eff.isMobile())
00141     --remain;
00142   unsigned int unused=remain;
00143   if(eff.getParent()!=NULL)
00145   if(eff.isMobile()) {
00147     //cout << "Link " << remain-unused << " " << eff.outputOffset << " range " << eff.qmin << " to " << eff.qmax << endl;
00148   }
00149   return unused;
00150 }
00151
00153   // transform from world to joint origin (not applying link tranform, so as if q=0)
00155   fmat::Column<3> cObj = tr*Pobj; // so objective in 'native' reference frame
00157
00158   //cout << "LINK1 " << cObj << ' ' << cEff << endl;
00159
00160   if(std::abs(cEff[0])<EPSILON && std::abs(cEff[1])<EPSILON) {
00161     //Plink is along axis of rotation - nothing we do is going to move it, so don't move at all
00162     valid=true; //debatable
00163     return;
00164   }
00165   if(std::abs(cObj[0])<EPSILON && std::abs(cObj[1])<EPSILON) {
00166     //objective point is along axis of rotation - current location is as good as any, so don't move at all
00167     valid=true; //debatable
00168     return;
00169   }
00170   float ao=atan2(cObj[1],cObj[0]); // angle of objective
00171   float ae=atan2(cEff[1],cEff[0]); // offset caused by effector
00173 }
00174
00176   // transform from world to joint origin (not applying link tranform, so as if q=0)
00178   fmat::Column<3> cObj = tr*Pobj; // so objective in 'native' reference frame
00179   //cout << "LINK1 " << cObj << ' ' << endl;
00181 }
00182
00183 /*! assumes that z axis of parent frame and current frame are either parallel or orthogonal
00184  *
00185  *  todo: would be nice if this could handle intermediary angles... currently anything not parallel is assumed to be orthogonal */
00186 void IKThreeLink::computeSecondLinkRevolute(KinematicJoint& curlink, const fmat::Column<3>& Pobj, KinematicJoint& endlink, const fmat::Column<3>& Plink, bool invert, bool& valid) const {
00187   // link point in current frame
00189   if(std::abs(cEff[0])<EPSILON && std::abs(cEff[1])<EPSILON) {
00190     //Plink is along axis of rotation - nothing we do is going to move it, so don't move at all
00191     valid=true; //debatable
00192     return;
00193   }
00194
00195   //cout << "LINK2 " << cEff << endl;
00196
00197   // object point in parent frame
00198   fmat::Column<3> pObj = curlink.getParent()->getFullInvT()*Pobj;
00199
00200   if(std::abs(curlink.getRotation()(2,2)) >= (1-EPSILON)) {
00201     // z axis parallel
00202
00203     // law of cosines
00204     //fmat::Column<3> pCur = curlink.getPosition();
00206     float adj2 = hypotf(cEff[0],cEff[1]);
00207     float opp = hypotf(pObj[0],pObj[1]);
00208     float a;
00210       // objective too close in -- fold up
00211       //cout << "too close!" << endl;
00212       a = 0;
00213     } else if(adj1 + adj2 <= opp) {
00214       // objective too far away -- extend out
00215       //cout << "too far!" << endl;
00216       a = (float)M_PI;
00217     } else {
00219       //cout << "Law of cosines: " << adj1 << ' ' << adj2 << ' ' << opp << " = " << ca << endl;
00220       a = std::acos(ca);
00221     }
00222
00223     float offset = -curlink.qOffset - std::atan2(cEff[1],cEff[0]);
00225       offset+=(float)M_PI;
00226
00227     // solutions are offset±a
00228     float q1,q2;
00229     if(invert) {
00230       q1 = offset-a;
00231       q2 = offset+a;
00232     } else {
00233       q1 = offset+a;
00234       q2 = offset-a;
00235     }
00236
00237     //std::cout << "angle " << a << " (" << a/M_PI*180 << "°) offset " << offset << " q1 " << q1 << " (" << q1/M_PI*180 << "°)" << " q2 " << q2 << " (" << q2/M_PI*180 << "°)" << std::endl;
00238     valid = curlink.tryQ( normalize_angle(q1) );
00239     if(!valid) { // try other solution
00240       //cout << "trying inverse" << endl;
00241       valid = curlink.tryQ( normalize_angle(q2) );
00242       if(!valid) { // go back
00243         //cout << "reverting inverse" << endl;
00244         valid = curlink.tryQ( normalize_angle(q1) );
00245       }
00246     } else if(std::abs(q1-q2)>EPSILON && curlink.qmin<=q2 && q2<=curlink.qmax) {
00247       //cout << "has inverse" << endl;
00248       hasInverseSolution=true;
00249       inverseKnee=q2;
00250     }
00251
00252   } else {
00253     // Assume orthogonal curlink z and parent z
00254     // Need to solve for q: (cr + coh/tan(q))² + lz² = por²
00255     // Simplifies to quadratic: x² + 2·cr·x + cr² + lz² - por² = 0, where x=coh/tan(q)
00256
00257     float cr = std::abs((float)curlink.r); // radius from parent z aka hip length
00258     //float lr = hypotf(cEff[0],cEff[1]); // radius of end link aka effector about current z
00259     float lz = std::abs(cEff[2]); // effector offset along current z
00260     float coh = pObj[2] - curlink.d; // height of objective from plane of current link about parent z
00262       coh=-coh; // sign depends on direction of parent z vs. curlink
00263     float por = hypotf(pObj[0],pObj[1]); // radius of objective from parent z
00264     //cout << "cr " << cr << " lz " << lz << " coh " << coh << " por " << por << endl;
00265
00266     float x;
00267     if(por<lz) {
00268       // no solution — effector is too far to the side (b*b - 4*c) < 0
00269       //cout << "no sol" << endl;
00270       // stay at current position?
00271       valid=false; //debatable
00272       return;
00273     } else if(false /*por < cr*/) {
00274       // above the hip, choose "inner" solution
00275       //cout << "inner" << endl;
00276       //float a = 1
00277       float b = 2*cr;
00278       float c = cr*cr + lz*lz - por*por;
00279       x = ( -b + std::sqrt(b*b - 4*c) ) / 2;
00280       //cout << "b " << b << " c " << c << " x " << x << endl;
00281     } else {
00282       // outside the hip, choose "outer" solution
00283       //cout << "outer" << endl;
00284       //float a = 1
00285       float b = 2*cr;
00286       float c = cr*cr + lz*lz - por*por;
00287       x = ( -b + std::sqrt(b*b - 4*c) ) / 2;
00288       //cout << "b " << b << " c " << c << " x " << x << endl;
00289     }
00290     //if(cEff[0]<0)
00291     //  x=-x;
00292
00293     // x = coh / tan(q), solve for q:
00294     float q = std::atan2(coh, x);
00295     //cout << "pre offset q " << q << " offset " << -std::atan2(cEff[1],cEff[0]) << endl;
00296     q -= curlink.qOffset + std::atan2(cEff[1],cEff[0]); // cancel offsets from effector point and qOffset
00297     valid = curlink.tryQ( normalize_angle(q) );
00298   }
00299 }
00300
00301 /*! First link will perform some rotation, this function needs to set
00302  *  the distance to match the projection of the objective into the
00303  *  plane of rotation.  Very similar to computeThirdLinkPrismatic()
00304  *  except how this projection is done (no ring of motion) */
00305 void IKThreeLink::computeSecondLinkPrismatic(KinematicJoint& curlink, const fmat::Column<3>& Pobj, KinematicJoint& endlink, const fmat::Column<3>& Plink, bool invert, bool& valid) const {
00306   KinematicJoint* tilt = curlink.getParent();
00307   while(!tilt->isMobile())
00308     tilt=tilt->getParent();
00309
00310   // object point in pan's frame
00311   fmat::Column<3> tObj = tilt->getFullInvT()*Pobj;
00312   //cout << "tObj " << tObj << endl;
00313
00314   fmat::Transform curToTilt = curlink.getParent()->getT(*tilt) * curlink.getTo();
00315
00316   const fmat::Column<3>& neck = curToTilt.translation();
00317   float neckD2 = neck.sumSq();
00318
00319   const fmat::Column<3>& tcz = curToTilt.column(2);
00320   float inner = fmat::dotProduct(tcz,-neck); // a·b·cos(C), |tcz|=1 so actually neckD·cos(C)
00321   //cout << "neck " << std::sqrt(neckD2) << " inner " << inner << endl;
00322
00323   float tod2 = tObj[0]*tObj[0] + tObj[1]*tObj[1];
00324   //cout << "tod " << std::sqrt(tod2) << endl;
00325
00327 }
00328
00329
00330 /*! We'll compute the knee angle first, using:
00331  *  - length of the thigh ('thigh')
00332  *  - distance from knee (curlink) to Plink, projected to plane of curlink's rotation (cEffD)
00333  *  - distance from shoulder (previous link) to Pobj (ringObjD)
00334  *
00335  *  Two knee (curlink) configurations are supported: parallel to parLink (where parLink.d can be bundled as a z offset of the effector)
00336  *  or parallel to grandparent, with parLink.r==0.
00337  *  In the former case, the parent link's origin forms a ring about the grandparent's
00338  *  link of radius parLink.r.  It is the distance of the objective to the ring (or just the center point
00339  *  when parLink.r==0) that curlink must match the effector's distance.
00340  */
00341 void IKThreeLink::computeThirdLinkRevolute(KinematicJoint& curlink, const fmat::Column<3>& Pobj, KinematicJoint& endlink, const fmat::Column<3>& Plink, bool invert, bool& valid) const {
00342   // link point in current frame
00344   if(std::abs(cEff[0])<EPSILON && std::abs(cEff[1])<EPSILON) {
00345     //Plink is along axis of rotation - nothing we do is going to move it, so don't move at all
00346     valid=true; //debatable
00347     return;
00348   }
00349   //cout << curlink.outputOffset << " cEff " << cEff << " dist " << cEff.norm() << " (in plane " << fmat::SubVector<2>(cEff).norm() << ")" << endl;
00350
00352   //std::cout << "par to cur dist " << curlink.getPosition().norm() << ' ' << curlink.getPosition() << std::endl;
00354   //std::cout << "check ring r " << parLink.r << ' ' << (parLink.getPosition() - fmat::pack(0,0,parLink.d)).norm() << std::endl;
00355
00356   // object point in grandparent's frame
00357   fmat::Column<3> gpObj = gparLink.getFullInvT()*Pobj;
00358   gpObj[2]-=parLink.d; // subtract parLink height so now relative to plane of ring of motion
00359   //cout << "gpObj " << gpObj << endl;
00360
00361   float thigh, thigh2; // distance from parent to current, projected to curlink's plane of rotation
00362   float ringObjD2; // distance from parent's ring of motion to objective, projected to curlink's plane of rotation
00363
00364   // check condition α = ±90° (e.g. Aibo legs, knee is perpendicular to elevator)
00365   const bool KNEE_PERP = std::abs(std::abs((float)curlink.alpha) - (float)M_PI_2) < EPSILON;
00366   if(KNEE_PERP) {
00367     // math gets wonky if parLink.r≠0, effector's z component non-planar to objective radial, don't know how to recover
00368     ASSERT(parLink.r==0,"IKThreeLink can't handle perpendicular knees with non-zero hip radius");
00369
00371     thigh = std::sqrt(thigh2);
00372
00373     float lz = cEff[2]; // distance of link point from knee rotation plane
00374     float rd = std::sqrt(gpObj[0]*gpObj[0] + gpObj[1]*gpObj[1]) - std::abs((float)parLink.r); // distance from ring of motion in grandparent xy
00375     ringObjD2 = rd*rd + gpObj[2]*gpObj[2] - lz*lz;
00376     if(ringObjD2<0)
00377       ringObjD2=0;
00378
00379   } else { // assume Chiara legs, knee is parallel to elevator
00380     //cout << "PARALLEL" << endl;
00381     thigh = std::abs((float)curlink.r);
00382     thigh2=thigh*thigh;
00383
00384     float lz = cEff[2] - curlink.d; // distance of link point from knee rotation plane
00385
00386     // now account for curlink.d and any pl.z, need to project objective distance onto curlink plane of rotation
00387     // grandparent origin, curlink origin, and objective form right triangle.  curlink-objective is parallel to grandparent xy
00388     float d2 = gpObj[0]*gpObj[0] + gpObj[1]*gpObj[1] - lz*lz;
00389     if(d2<0)
00390       d2=0;
00391     float rd = std::sqrt(d2) - std::abs((float)parLink.r); // distance from ring of motion in grandparent xy
00392     //cout << "d2 " << d2 << " lz " << lz << " rd " << rd << endl;
00393     ringObjD2 = rd*rd + gpObj[2]*gpObj[2];
00394
00395     /* Singularity issue: current implementation cannot reach points "behind" (x<0) the hip because we are
00396      setting oringd to be the closest point on the *ring*, but with hip rotation limits we actually have an *arc*.
00397      However this limitation prevents a singularity as the target point passes behind the hip, we would have to
00398      instantly snap 180° to the opposite side of the arc to continue tracking.  Instead what we do here is
00399      to allow the hip to hit its joint limit and just stay there on the same side, even though technically the
00400      point is within reach.  We could do something like add a flag to allow this solution in case the instantanious
00401      motion is acceptable. */
00402   }
00403   float ringObjD = std::sqrt(ringObjD2);
00404
00405   // distance from curlink to pLink projected to curlink's xy plane:
00406   float ln2 = cEff[0]*cEff[0] + cEff[1]*cEff[1];
00407   float ln = std::sqrt(ln2);
00408
00409   //cout << "thigh " << thigh << " oringd " << ringObjD << " link " << ln << endl;
00410
00411   // test for reachability otherwise angle will be NaN
00412   float a;
00413   if( (ln + ringObjD <= thigh) || (ringObjD + thigh <= ln)) {
00414     //cout << "out of reach, interior" << endl;
00415     a = 0;
00416
00417   } else if(ln + thigh <= ringObjD) {
00418     //cout << "out of reach, exterior" << endl;
00419     a = (float)M_PI;
00420
00421   } else {
00422     // law of cosines to find angle between parent and solution point
00423     a = std::acos( (ln2 + thigh2 - ringObjD2) / (2*ln*thigh) );
00424   }
00425   float offset = -curlink.qOffset - std::atan2(cEff[1],cEff[0]);
00426
00427   if(KNEE_PERP) {
00430     else
00432     a = (float)M_PI-a;
00433   } else if(curlink.r>0) {
00434     a = (float)M_PI-a;
00435   }
00436
00437   // solutions are offset ± a
00438   float q1,q2;
00439   if(invert) {
00440     q1 = offset-a;
00441     q2 = offset+a;
00442   } else {
00443     q1 = offset+a;
00444     q2 = offset-a;
00445   }
00446   //std::cout << "angle " << a << " (" << a/M_PI*180 << "°) offset " << offset << " q1 " << q1 << " (" << q1/M_PI*180 << "°) q2 " << q2 << " (" << q2/M_PI*180 << ")" << std::endl;
00447
00448   valid = curlink.tryQ( normalize_angle(q1) );
00449   if(!valid) { // try other solution
00450     //cout << "trying inverse" << endl;
00451     valid = curlink.tryQ( normalize_angle(q2) );
00452     if(!valid) { // go back
00453       //cout << "reverting inverse" << endl;
00454       valid = curlink.tryQ( normalize_angle(q1) );
00455     }
00456   } else if(std::abs(q1-q2)>EPSILON && curlink.qmin<=q2 && q2<=curlink.qmax) {
00457     //cout << "has inverse" << endl;
00458     hasInverseSolution=true;
00459     inverseKnee=q2;
00460   }
00461 }
00462
00463 /*! This version is intended mainly for solving to have a pan-tilt camera look at a point in space.
00464  *  The camera is the third link, and should be marked as prismatic.  There can be immobile
00465  *  frames between this and the tilt joint, as we often want a specific configuration of x and y axes
00466  *  to match image coordinates.  Z must point out of the camera, toward the scene.
00467  *
00468  *  @a Plink is ignored.  This only solves for the z axis, and that must intersect the pan axis.
00469  *  (The math gets really hairy if the camera is not aligned to a radial from the pan joint.)
00470  *
00471  *  The approach is similar to computeThirdLinkRevolute(), using
00472  *  - length of the neck, i.e. distance from tilt to camera
00473  *  - angle at the camera (curlink) between neck link and z axis
00474  *  - distance from tilt's ring of motion to Pobj (ringObjD)
00475  */
00476 void IKThreeLink::computeThirdLinkPrismatic(KinematicJoint& curlink, const fmat::Column<3>& Pobj, KinematicJoint& endlink, const fmat::Column<3>&, bool invert, bool& valid) const {
00477   //cout << "PRISMATIC" << endl;
00478
00479   KinematicJoint* tilt = curlink.getParent();
00480   while(!tilt->isMobile())
00481     tilt=tilt->getParent();
00482   KinematicJoint* pan = tilt->getParent();
00483
00484   // object point in pan's frame
00485   fmat::Column<3> gpObj = pan->getFullInvT()*Pobj;
00486   gpObj[2]-=tilt->d; // subtract tilt height so now relative to plane of ring of motion
00487   //cout << "gpObj " << gpObj << endl;
00488
00489   fmat::Transform curToTilt = curlink.getParent()->getT(*tilt) * curlink.getTo();
00490
00491   const fmat::Column<3>& neck = curToTilt.translation();
00492   float neckD2 = neck.sumSq();
00493
00494   const fmat::Column<3>& tcz = curToTilt.column(2);
00495   float inner = fmat::dotProduct(tcz,-neck); // a·b·cos(C), |tcz|=1 so actually neckD·cos(C)
00496   //cout << "neck " << std::sqrt(neckD2) << " inner " << inner << endl;
00497
00498   float d2 = gpObj[0]*gpObj[0] + gpObj[1]*gpObj[1];
00499   float rd = std::sqrt(d2) - std::abs((float)tilt->r); // distance from ring of motion in grandparent xy
00500   float ringObjD2 = rd*rd + gpObj[2]*gpObj[2];
00501   //cout << "d " << std::sqrt(d2) << " rd " << rd << " ringObjD " << std::sqrt(ringObjD2) << endl;
00502
00504 }
00505
00506 fmat::fmatReal IKThreeLink::computePrismaticQ(fmat::fmatReal objD2, fmat::fmatReal neckD2, fmat::fmatReal inner) {
00507   // law of cosines: c² = a² + b² - 2·a·b·cos(C)
00508   // where c = ringObjD, a = neckD, a·b·cos(C) = inner, solve for b:
00509   // b² - 2·a·b·cos(C) + a² - c² = 0
00510   // apply quadratic formula:
00511   // a'x² + b'x + c' = 0, x = (-b' ± √(b'² - 4·a'·c'))/(2a')
00512   // where a' = 1, b' = -2·neckD·cos(C), c' = neckD2 - ringObjD2, x=b
00513   float b = -2*inner;
00514   float c = neckD2 - objD2;
00515   float r = b*b - 4*c;
00516   if(r<0) {
00517     //cout << "NO SOLUTION" << endl;
00518     // closest solution is at right angle to objD extension, aka inner
00519     float q2 = neckD2 - inner*inner;
00520     if(q2<0) {
00521       // should only happen due to numerical roundoff, if ever
00522       cerr << "IKThreeLink::computePrismaticQ numeric error, q² < 0 : " << q2 << " from " << neckD2 << ' ' << inner << ' ' << objD2 << endl;
00523       return 0;
00524     } else {
00525       return std::sqrt(q2);
00526     }
00527   } else {
00528     return (-b + std::sqrt(r))/2; // only want largest/positive solution
00529   }
00530 }
00531
00532
00533
00534 /*! @file
00535  * @brief
00536  * @author Ethan Tira-Thompson (ejt) (Creator)
00537  */
00538
00539 #endif
```

 Tekkotsu v5.1CVS Generated Mon May 9 04:58:42 2016 by Doxygen 1.6.3