netft_utils_lean.cpp
Go to the documentation of this file.
00001 
00002 /*
00003 Copyright (c) 2016, Los Alamos National Security, LLC
00004 All rights reserved.
00005 Copyright 2016. Los Alamos National Security, LLC. This software was produced under U.S. Government contract DE-AC52-06NA25396 for Los Alamos National Laboratory (LANL), which is operated by Los Alamos National Security, LLC for the U.S. Department of Energy. The U.S. Government has rights to use, reproduce, and distribute this software.  NEITHER THE GOVERNMENT NOR LOS ALAMOS NATIONAL SECURITY, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE.  If software is modified to produce derivative works, such modified software should be clearly marked, so as not to confuse it with the version available from LANL.
00006 
00007 Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
00008 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
00009 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 
00010 3. Neither the name of Los Alamos National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 
00011 
00012 THIS SOFTWARE IS PROVIDED BY LOS ALAMOS NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LOS ALAMOS NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
00013 
00014 Author: Alex von Sternberg
00015 */
00016 
00017 #include "netft_utils_lean.h"
00018 
00019 NetftUtilsLean::NetftUtilsLean(ros::NodeHandle* nh) :
00020   n(nh),
00021   ftAddress(""),
00022   ftTopic(""),
00023   isInit(false),
00024   hasData(false),
00025   cycleRate(0.0),
00026   isFilterOn(false),
00027   deltaTFilter(0.0),
00028   cutoffFrequency(0.0),
00029   newFilter(false),
00030   lpExists(false),
00031   isBiased(false),
00032   isNewBias(false),
00033   waitingForTransform(true),
00034   isActive(false),
00035   cancel_count(MAX_CANCEL),
00036   cancel_wait(MAX_WAIT),
00037   forceMaxB(10.0),
00038   torqueMaxB(0.8),
00039   forceMaxU(50.0),
00040   torqueMaxU(5.0),
00041   toUpdate(false),
00042   toMonitor(false)
00043 {
00044 }
00045 
00046 NetftUtilsLean::~NetftUtilsLean()
00047 {
00048   delete listener;
00049   if(lpExists)
00050     delete lp;
00051 }
00052 
00053 bool NetftUtilsLean::initialize(double rate, std::string world, std::string ft, double force, double torque)
00054 {
00055   // Set the user input
00056   if(!setUserInput(world, ft, force, torque))
00057   {
00058     return false;
00059   }
00060   // Set the cycle rate
00061   if(rate>0.001)
00062   {
00063     cycleRate = rate;
00064   }
00065   else
00066   {
00067     ROS_ERROR_STREAM("Cycle rate should positive");
00068     return false;
00069   }
00070   
00071   // Set up access to netft data
00072   if(!ftTopic.empty())
00073   {
00074     ft_sub = n->subscribe<geometry_msgs::WrenchStamped>(ftTopic, 1, &NetftUtilsLean::dataCallback, this);
00075     ROS_INFO_STREAM("Using NETFT topic instead of IP because ftTopic is:" << ftTopic);
00076   }
00077   else if(!ftAddress.empty())
00078   {
00079     try
00080     {
00081       netft = std::unique_ptr<netft_rdt_driver::NetFTRDTDriver>(new netft_rdt_driver::NetFTRDTDriver(ftAddress));
00082     }
00083     catch(std::runtime_error)
00084     {
00085       ROS_ERROR_STREAM("Could not access data from netft_rdt_driver");
00086       return false;
00087     }
00088   }
00089   else
00090   {
00091       ROS_ERROR_STREAM("Can not initialize FT, must set topic or address first.");
00092       return false;
00093   }
00094     
00095   
00096   //Zero out the zero wrench
00097   zero_wrench.wrench.force.x = 0.0;
00098   zero_wrench.wrench.force.y = 0.0;
00099   zero_wrench.wrench.force.z = 0.0;
00100   zero_wrench.wrench.torque.x = 0.0;
00101   zero_wrench.wrench.torque.y = 0.0;
00102   zero_wrench.wrench.torque.z = 0.0;
00103 
00104   //Initialize cancel message
00105   cancel_msg.toCancel = false;
00106 
00107   //Listen to the transfomation from the ft sensor to world frame.
00108   listener = new tf::TransformListener(ros::Duration(300));
00109 
00110   //Publish on the /cancel topic. Queue up to 100000 data points
00111   netft_cancel_pub = n->advertise<netft_utils::Cancel>("/netft/cancel", 100000);
00112   
00113   if(DEBUG_DATA)
00114     data_pub = n->advertise<geometry_msgs::WrenchStamped>("/netft/netft_data", 100000);
00115   
00116   isInit = true;
00117   return true;
00118 }
00119 
00120 bool NetftUtilsLean::run()
00121 {
00122   if(!isInit)
00123   {
00124     ROS_ERROR_STREAM("Cannot run before initialization is successful.");
00125     return false;
00126   }
00127   
00128   isActive = true;
00129   
00130   toUpdate = true;
00131   toMonitor = true;
00132   
00133   //Spin off update thread
00134   updateThread = std::async(std::launch::async, &NetftUtilsLean::update, this);
00135   
00136   //Spin off thread to monitor netft data
00137   if(!ftAddress.empty())
00138   {
00139     monitorThread = std::async(std::launch::async, &NetftUtilsLean::monitorData, this);
00140   }
00141   
00142   //Join threads
00143   if(!ftAddress.empty())
00144   {
00145     monitorThread.get();
00146   }
00147   updateThread.get();
00148   
00149   isActive = false;
00150   return true;
00151 }
00152 
00153 void NetftUtilsLean::stop()
00154 {
00155   toUpdate = false;
00156   toMonitor = false;
00157 }
00158 
00159 bool NetftUtilsLean::monitorData()
00160 {
00161   while(waitingForTransform)
00162   {
00163     ros::Duration(0.05).sleep();
00164   }
00165   ros::Rate r(cycleRate);
00166   while (ros::ok() && toMonitor)
00167   {
00168     //ros::Time tempTime = ros::Time::now();
00169     if (netft->waitForNewData())
00170     {
00171       //std::cout << "Time waiting for new data: " <<ros::Time::now().toSec()-tempTime.toSec() << std::endl;
00172       geometry_msgs::WrenchStamped data;
00173       //tempTime = ros::Time::now();
00174       netft->getData(data);
00175       //std::cout << "Time to get data: " <<ros::Time::now().toSec()-tempTime.toSec() << std::endl;
00176       //tempTime = ros::Time::now();
00177       netftCallback(data);
00178       //std::cout << "Time in netftCallback: " <<ros::Time::now().toSec()-tempTime.toSec() << std::endl;
00179       hasData = true;
00180     }
00181     //std::cout << "Monitoring!!!!!!" << std::endl;
00182     //tempTime = ros::Time::now();
00183     r.sleep();
00184     //std::cout << "Time sleeping: " <<ros::Time::now().toSec()-tempTime.toSec() << std::endl;
00185   }
00186   hasData = false;
00187   return true;
00188 }
00189 
00190 void NetftUtilsLean::dataCallback(const geometry_msgs::WrenchStamped::ConstPtr& msg)
00191 {
00192   if(!waitingForTransform && toMonitor)
00193   {
00194     raw_topic_data;
00195     raw_topic_data.header = msg->header;
00196     raw_topic_data.wrench = msg->wrench;
00197     netftCallback(raw_topic_data);
00198     hasData = true;
00199   }
00200   else
00201     hasData = false;
00202 }
00203 
00204 bool NetftUtilsLean::isReady()
00205 {
00206   return hasData;
00207 }
00208 
00209 bool NetftUtilsLean::waitForData(double timeout)
00210 {
00211   ros::Time startWait = ros::Time::now();
00212   while((ros::Time::now().toSec() - startWait.toSec()) < timeout && !hasData)
00213   {
00214     ros::Duration(0.001).sleep();
00215   }
00216   return hasData;
00217 }
00218 
00219 bool NetftUtilsLean::setUserInput(std::string world, std::string ft, double force, double torque)
00220 {
00221   if(world.compare("") == 0)
00222   {
00223     ROS_ERROR_STREAM("World frame string cannot be empty");
00224     return false;
00225   }
00226   world_frame = world;
00227   ROS_INFO_STREAM("World frame: " << world_frame);
00228   if(ft.compare("") == 0)
00229   {
00230     ROS_ERROR_STREAM("FT frame string cannot be empty");
00231     return false;
00232   }
00233   ft_frame = ft;
00234   ROS_INFO_STREAM("FT frame: " << ft_frame);
00235   if(force >= 0.001)
00236   {
00237     forceMaxU = force;
00238   }
00239   else
00240   {
00241     ROS_ERROR_STREAM("FT max force must be positive");
00242     return false;
00243   }
00244   if(torque >= 0.001)
00245   {
00246     torqueMaxU = torque;
00247   }
00248   else
00249   {
00250     ROS_ERROR_STREAM("FT max torque must be positive");
00251     return false;
00252   }
00253   return true;
00254 }
00255 
00256 bool NetftUtilsLean::update()
00257 {
00258   if(!isInit)
00259   {
00260     ROS_ERROR_STREAM("Cannot update until NetftUtilsLean is initialized properly.");
00261     return false;
00262   }
00263   ros::Rate r(cycleRate);
00264   while (ros::ok() && toUpdate)
00265   {
00266     // Check for a filter
00267     if(newFilter)
00268     {
00269       if(lpExists)
00270         delete lp;
00271       lp = new LPFilter(deltaTFilter,cutoffFrequency,6);
00272       lpExists = true;
00273       newFilter = false;
00274     }
00275     
00276     // Look up transform from ft to world frame
00277     tf::StampedTransform tempTransform;
00278     try
00279     {
00280       listener->waitForTransform(world_frame, ft_frame, ros::Time(0), ros::Duration(1.0));
00281       listener->lookupTransform(world_frame, ft_frame, ros::Time(0), tempTransform);
00282     }
00283     catch (tf::TransformException ex)
00284     {
00285       ROS_ERROR("%s",ex.what());
00286     }
00287    
00288     // Set translation to zero before updating value
00289     tempTransform.setOrigin(tf::Vector3(0.0,0.0,0.0));
00290     ft_to_world = tempTransform;
00291     waitingForTransform = false;
00292    
00293     checkMaxForce();
00294    
00295     // Publish cancel_msg
00296     netft_cancel_pub.publish( cancel_msg );
00297     r.sleep();
00298   } 
00299   return true;
00300 }
00301 
00302 void NetftUtilsLean::copyWrench(geometry_msgs::WrenchStamped &in, geometry_msgs::WrenchStamped &out, geometry_msgs::WrenchStamped &diff)
00303 {
00304   out.header.stamp = in.header.stamp;
00305   out.header.frame_id = in.header.frame_id;
00306   out.wrench.force.x = in.wrench.force.x - diff.wrench.force.x;
00307   out.wrench.force.y = in.wrench.force.y - diff.wrench.force.y;
00308   out.wrench.force.z = in.wrench.force.z - diff.wrench.force.z;
00309   out.wrench.torque.x = in.wrench.torque.x - diff.wrench.torque.x;
00310   out.wrench.torque.y = in.wrench.torque.y - diff.wrench.torque.y;
00311   out.wrench.torque.z = in.wrench.torque.z - diff.wrench.torque.z;
00312 }
00313 
00314 void NetftUtilsLean::applyThreshold(double &value, double thresh)
00315 {
00316   if(value <= thresh && value >= -thresh)
00317   {
00318     value = 0.0;
00319   }
00320 }
00321 
00322 void NetftUtilsLean::transformFrame(geometry_msgs::WrenchStamped in_data, geometry_msgs::WrenchStamped &out_data, char target_frame)
00323 {
00324   tf::Vector3 tempF;
00325   tf::Vector3 tempT;
00326   tempF.setX(in_data.wrench.force.x);
00327   tempF.setY(in_data.wrench.force.y);
00328   tempF.setZ(in_data.wrench.force.z);
00329   tempT.setX(in_data.wrench.torque.x);
00330   tempT.setY(in_data.wrench.torque.y);
00331   tempT.setZ(in_data.wrench.torque.z);
00332   if(target_frame == 'w')
00333   {
00334       out_data.header.frame_id = world_frame;
00335       tempF = ft_to_world * tempF;
00336       tempT = ft_to_world * tempT;
00337   }
00338   else if(target_frame == 't')
00339   {
00340       out_data.header.frame_id = ft_frame;
00341       tempF = ft_to_world.inverse() * tempF;
00342       tempT = ft_to_world.inverse() * tempT;
00343   }     
00344   out_data.header.stamp = in_data.header.stamp;
00345   out_data.wrench.force.x = tempF.getX();
00346   out_data.wrench.force.y = tempF.getY();
00347   out_data.wrench.force.z = tempF.getZ();
00348   out_data.wrench.torque.x = tempT.getX();
00349   out_data.wrench.torque.y = tempT.getY();
00350   out_data.wrench.torque.z = tempT.getZ();
00351 }
00352 
00353 void NetftUtilsLean::netftCallback(const geometry_msgs::WrenchStamped& data)
00354 {
00355   // Filter data. apply negative to x data to follow right hand rule convention (ft raw data does not)
00356   std::vector<double> tempData;
00357   tempData.resize(6);
00358   if(!ftAddress.empty())
00359   {
00360     tempData.at(0) = -data.wrench.force.x;
00361     tempData.at(1) = data.wrench.force.y;
00362     tempData.at(2) = data.wrench.force.z;
00363     tempData.at(3) = -data.wrench.torque.x;
00364     tempData.at(4) = data.wrench.torque.y;
00365     tempData.at(5) = data.wrench.torque.z;
00366   }
00367   else
00368   {
00369     tempData.at(0) = data.wrench.force.x;
00370     tempData.at(1) = data.wrench.force.y;
00371     tempData.at(2) = data.wrench.force.z;
00372     tempData.at(3) = data.wrench.torque.x;
00373     tempData.at(4) = data.wrench.torque.y;
00374     tempData.at(5) = data.wrench.torque.z;
00375   }
00376   
00377   if(isFilterOn && !newFilter)
00378     lp->update(tempData,tempData);
00379   
00380   // Copy tool frame data. 
00381   raw_data_tool.header.stamp = data.header.stamp;
00382   raw_data_tool.header.frame_id = ft_frame;
00383   raw_data_tool.wrench.force.x = tempData.at(0);
00384   raw_data_tool.wrench.force.y = tempData.at(1);
00385   raw_data_tool.wrench.force.z = tempData.at(2);
00386   raw_data_tool.wrench.torque.x = tempData.at(3);
00387   raw_data_tool.wrench.torque.y = tempData.at(4);
00388   raw_data_tool.wrench.torque.z = tempData.at(5);
00389   
00390   // Apply bias
00391   copyWrench(raw_data_tool, tf_data_tool, bias); 
00392   
00393   // Copy in new netft data in tool frame and transform to world frame
00394   transformFrame(tf_data_tool, tf_data_world, 'w');
00395   
00396   // Apply thresholds
00397   applyThreshold(tf_data_world.wrench.force.x, threshold.wrench.force.x);
00398   applyThreshold(tf_data_world.wrench.force.y, threshold.wrench.force.y);
00399   applyThreshold(tf_data_world.wrench.force.z, threshold.wrench.force.z);
00400   applyThreshold(tf_data_world.wrench.torque.x, threshold.wrench.torque.x);
00401   applyThreshold(tf_data_world.wrench.torque.y, threshold.wrench.torque.y);
00402   applyThreshold(tf_data_world.wrench.torque.z, threshold.wrench.torque.z);
00403   applyThreshold(tf_data_tool.wrench.force.x, threshold.wrench.force.x);
00404   applyThreshold(tf_data_tool.wrench.force.y, threshold.wrench.force.y);
00405   applyThreshold(tf_data_tool.wrench.force.z, threshold.wrench.force.z);
00406   applyThreshold(tf_data_tool.wrench.torque.x, threshold.wrench.torque.x);
00407   applyThreshold(tf_data_tool.wrench.torque.y, threshold.wrench.torque.y);
00408   applyThreshold(tf_data_tool.wrench.torque.z, threshold.wrench.torque.z);
00409   
00410   // Publish data for debugging
00411   if(DEBUG_DATA)
00412     data_pub.publish(tf_data_tool);
00413   //ROS_INFO_STREAM("Callback time: " << tf_data_tool.header.stamp.toSec()-ros::Time::now().toSec());
00414 }                 
00415                   
00416 bool NetftUtilsLean::biasSensor(bool toBias)
00417 {                 
00418   if(toBias)  
00419   {               
00420     if(!hasData)
00421     {
00422       geometry_msgs::WrenchStamped data;
00423       if(!ftTopic.empty())
00424       {
00425         ros::Time startTime = ros::Time::now();
00426         while(ros::Time::now().toSec()-startTime.toSec() < 0.1 && !hasData)
00427         {
00428           ros::Duration(0.01).sleep();
00429         }
00430         if(hasData)
00431         {
00432           data = raw_topic_data;
00433         }
00434         else
00435         {
00436           ROS_ERROR("Bias sensor failed.");
00437           return false;
00438         }
00439       }
00440       else
00441       {
00442         if (netft->waitForNewData())
00443         {
00444           netft->getData(data);
00445         }
00446         else
00447         {
00448           ROS_ERROR("Bias sensor failed.");
00449           return false;
00450         }
00451       }           
00452       // Copy tool frame data. 
00453       raw_data_tool.header.stamp = data.header.stamp;
00454       raw_data_tool.header.frame_id = ft_frame;
00455       raw_data_tool.wrench.force.x = -data.wrench.force.x;
00456       raw_data_tool.wrench.force.y = data.wrench.force.y;
00457       raw_data_tool.wrench.force.z = data.wrench.force.z;
00458       raw_data_tool.wrench.torque.x = -data.wrench.torque.x;
00459       raw_data_tool.wrench.torque.y = data.wrench.torque.y;
00460       raw_data_tool.wrench.torque.z = data.wrench.torque.z;
00461     }
00462     copyWrench(raw_data_tool, bias, zero_wrench);
00463     isNewBias = true;
00464   }               
00465   else            
00466   {               
00467     copyWrench(zero_wrench, bias, zero_wrench);
00468   }               
00469   isBiased = toBias;
00470   return true;    
00471 }   
00472 
00473 bool NetftUtilsLean::setFilter(bool toFilter, double deltaT, double cutoffFreq)
00474 {                 
00475   if(toFilter)  
00476   {
00477     newFilter = true;
00478     isFilterOn = true;
00479     deltaTFilter = deltaT;
00480     cutoffFrequency = cutoffFreq;
00481   }               
00482   else            
00483   {               
00484     isFilterOn = false;
00485   }               
00486   
00487   return true;    
00488 }  
00489 
00490 bool NetftUtilsLean::setMax(double fMaxU, double tMaxU, double fMaxB, double tMaxB)
00491 {                 
00492   if(fMaxU >= 0.0001)
00493   {
00494     forceMaxU = fMaxU;
00495     torqueMaxU = tMaxU;
00496     forceMaxB = fMaxB;
00497     torqueMaxB = tMaxB;
00498     return true;
00499   }
00500   else
00501   {
00502     ROS_ERROR_STREAM("All maximum FT values must be positive.");
00503     return false;
00504   }
00505 }  
00506 
00507 bool NetftUtilsLean::setThreshold(double fThresh, double tThresh)
00508 {                 
00509   threshold.wrench.force.x = fThresh;
00510   threshold.wrench.force.y = fThresh;
00511   threshold.wrench.force.z = fThresh;
00512   threshold.wrench.torque.x = tThresh;
00513   threshold.wrench.torque.y = tThresh;
00514   threshold.wrench.torque.z = tThresh;
00515                   
00516   return true;    
00517 }
00518 
00519 void NetftUtilsLean::checkMaxForce()
00520 {
00521   double fMag = pow((pow(tf_data_tool.wrench.force.x, 2.0) + pow(tf_data_tool.wrench.force.y, 2.0) + pow(tf_data_tool.wrench.force.z, 2.0)), 0.5);
00522   double tMag = pow((pow(tf_data_tool.wrench.torque.x, 2.0) + pow(tf_data_tool.wrench.torque.y, 2.0) + pow(tf_data_tool.wrench.torque.z, 2.0)), 0.5);
00523   double fMax;
00524   double tMax;
00525   if(isBiased && !isNewBias)
00526   {
00527     if(forceMaxB > 0.001 && torqueMaxB > 0.001)
00528     {
00529       fMax = forceMaxB;
00530       tMax = torqueMaxB;
00531     }
00532     else
00533     {
00534       fMax = forceMaxU;
00535       tMax = torqueMaxU;
00536     }
00537   }
00538   else
00539   {
00540     if(isBiased && isNewBias)
00541     {
00542       isNewBias = false;
00543     }
00544     fMax = forceMaxU;
00545     tMax = torqueMaxU;
00546   }
00547 
00548   // If max FT exceeded, send cancel unless we have just sent it MAX_CANCEL times
00549   //ROS_INFO("FMAG: %f TMAG: %f", fMag, tMag);
00550   if((fabs(fMag) > fMax || fabs(tMag) > tMax) && cancel_count > 0)
00551   {
00552     cancel_msg.toCancel = true;
00553     //ROS_INFO("Force torque violation. Canceling move.");
00554     ROS_INFO("FMAG: %f FMAX: %f TMAG:%f TMAX: %f count: %d wait: %d", fMag, fMax, tMag, tMax, cancel_count, cancel_wait);
00555     cancel_count-=1;
00556   }
00557   // If we have sent cancel MAX_CANCEL times, don't send cancel again for MAX_WAIT cycles
00558   else if(cancel_count == 0 && cancel_wait > 0 && cancel_wait <= MAX_WAIT)
00559   {
00560     cancel_msg.toCancel = false;
00561     cancel_wait-=1;
00562   }
00563   // If we have just finished waiting MAX_WAIT times, or the max force is no longer exceeded, reset cancel_count and cancel_wait
00564   else if(cancel_wait == 0 || !(fabs(fMag) > fMax || fabs(tMag) > tMax))
00565   {
00566     cancel_msg.toCancel = false;
00567     cancel_count = MAX_CANCEL;
00568     cancel_wait = MAX_WAIT;
00569   }
00570 }
00571 
00572 void NetftUtilsLean::setFTAddress(std::string ftAd)
00573 {
00574   ftAddress = ftAd;
00575 }
00576 
00577 void NetftUtilsLean::setFTTopic(std::string ftTop)
00578 {
00579   ftTopic = ftTop;
00580 }
00581 
00582 void NetftUtilsLean::getRawData(geometry_msgs::WrenchStamped& data)
00583 {
00584   data = raw_data_tool;
00585 }
00586 
00587 void NetftUtilsLean::getToolData(geometry_msgs::WrenchStamped& data)
00588 {
00589   data = tf_data_tool;
00590 }
00591 
00592 void NetftUtilsLean::getWorldData(geometry_msgs::WrenchStamped& data)
00593 {
00594   data = tf_data_world;
00595 }
00596 
00597 bool NetftUtilsLean::isRunning()
00598 {
00599   return isActive;
00600 }
00601                   


netft_utils
Author(s): Alex von Sternberg , Derek King, Andy Zelenak
autogenerated on Sat Jul 15 2017 02:45:58