controller_tuner.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2011 Shadow Robot Company Ltd.
00004 #
00005 # This program is free software: you can redistribute it and/or modify it
00006 # under the terms of the GNU General Public License as published by the Free
00007 # Software Foundation, either version 2 of the License, or (at your option)
00008 # any later version.
00009 #
00010 # This program is distributed in the hope that it will be useful, but WITHOUT
00011 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
00012 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
00013 # more details.
00014 #
00015 # You should have received a copy of the GNU General Public License along
00016 # with this program.  If not, see <http://www.gnu.org/licenses/>.
00017 #
00018 
00019 import os, subprocess, math, time
00020 
00021 import roslib
00022 roslib.load_manifest('sr_gui_controller_tuner')
00023 import rospy, rosparam
00024 
00025 
00026 from qt_gui.plugin import Plugin
00027 from python_qt_binding import loadUi
00028 
00029 from QtCore import QEvent, QObject, Qt, QTimer, Slot, SIGNAL, QThread
00030 from QtGui import QWidget, QTreeWidgetItem, QCheckBox, QSpinBox, QDoubleSpinBox, QFileDialog, QMessageBox, QPushButton, QFrame, QHBoxLayout
00031 from functools import partial
00032 from tempfile import NamedTemporaryFile
00033 
00034 from sensor_msgs.msg import JointState
00035 
00036 from sr_gui_controller_tuner.sr_controller_tuner import SrControllerTunerApp
00037 
00038 class PlotThread(QThread):
00039     def __init__(self, parent = None, joint_name = "FFJ0", controller_type = "Motor Force"):
00040         QThread.__init__(self, parent)
00041         self.joint_name_ = joint_name
00042         self.is_joint_0_ = False
00043         self.joint_index_in_joint_state_ = None
00044         try:
00045             self.joint_index_in_joint_state_ = ["FFJ0", "MFJ0", "RFJ0", "LFJ0"].index(self.joint_name_)
00046             self.is_joint_0_ = True
00047         except ValueError:
00048             self.is_joint_0_ = False
00049 
00050         self.controller_type_ = controller_type
00051 
00052         #prepares the title for the plot (can't contain spaces)
00053         self.plot_title_ = self.joint_name_ + " " + self.controller_type_
00054         self.plot_title_ = self.plot_title_.replace(" ", "_").replace("/","_")
00055 
00056         #stores the subprocesses to be able to terminate them on close
00057         self.subprocess_ = []
00058 
00059     def run(self):
00060         rxplot_str = "rxplot -b 30 -p 30 --title=" + self.plot_title_ + " "
00061 
00062         if self.controller_type_ == "Motor Force":
00063             # the joint 0s are published on a different topic: /joint_0s/joint_states
00064             if not self.is_joint_0_:
00065                 #subscribe to joint_states to get the index of the joint in the message
00066                 self.subscriber_ = rospy.Subscriber("/joint_states", JointState, self.js_callback_)
00067 
00068                 #wait until we got the joint index in the joint
00069                 # states message
00070                 while self.joint_index_in_joint_state_ == None:
00071                     time.sleep(0.01)
00072 
00073                 rxplot_str += "/joint_states/effort["+ str(self.joint_index_in_joint_state_) +"]"
00074             else:
00075                 rxplot_str += "/joint_0s/joint_states/effort["+ str(self.joint_index_in_joint_state_) +"]"
00076 
00077         elif self.controller_type_ == "Position":
00078             rxplot_str += "sh_"+self.joint_name_.lower()+"_position_controller/state/set_point,sh_"+self.joint_name_.lower()+"_position_controller/state/process_value /sh_" + self.joint_name_.lower()+"_position_controller/state/command"
00079         elif self.controller_type_ == "Velocity":
00080             rxplot_str += "sh_"+self.joint_name_.lower()+"_velocity_controller/state/set_point,sh_"+self.joint_name_.lower()+"_velocity_controller/state/process_value"
00081         elif self.controller_type_ == "Mixed Position/Velocity":
00082             rxplot_str += "sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller/state/set_point,sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller/state/process_value sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller/state/process_value_dot,sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller/state/commanded_velocity sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller/state/command,sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller/state/measured_effort,sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller/state/friction_compensation"
00083         elif self.controller_type_ == "Effort":
00084             rxplot_str += "sh_"+self.joint_name_.lower()+"_effort_controller/state/set_point,sh_"+self.joint_name_.lower()+"_effort_controller/state/process_value"
00085 
00086         self.subprocess_.append( subprocess.Popen(rxplot_str.split()) )
00087 
00088     def js_callback_(self, msg):
00089         #get the joint index once, then unregister
00090         self.joint_index_in_joint_state_ = msg.name.index( self.joint_name_.upper() )
00091         self.subscriber_.unregister()
00092         self.subscriber_ = None
00093 
00094     def __del__(self):
00095         for subprocess in self.subprocess_:
00096             #killing the rxplot to close the window
00097             subprocess.kill()
00098 
00099         if self.subscriber_ != None:
00100             self.subscriber_.unregister()
00101             self.subscriber_ = None
00102 
00103         self.wait()
00104 
00105 class MoveThread(QThread):
00106     def __init__(self, parent = None, joint_name = "FFJ0", controller_type = "Motor Force"):
00107         QThread.__init__(self, parent)
00108         self.joint_name_ = joint_name
00109         self.controller_type_ = controller_type
00110         self.btn = parent
00111         self.subprocess_ = []
00112 
00113     def run(self):
00114         self.launch_()
00115 
00116     def __del__(self):
00117         for subprocess in self.subprocess_:
00118             subprocess.terminate()
00119         self.wait()
00120 
00121     def create_launch_file_(self):
00122         #Not using automatic move for velocity and effort controllers
00123         controller_name_ = ""
00124         if self.controller_type_ == "Position":
00125             controller_name_ = "sh_"+self.joint_name_.lower()+"_position_controller"
00126         elif self.controller_type_ == "Mixed Position/Velocity":
00127             controller_name_ = "sh_"+self.joint_name_.lower()+"_mixed_position_velocity_controller"
00128 
00129         min_max = self.get_min_max_()
00130 
00131         string = "<launch> <node pkg=\"sr_movements\" name=\"sr_movements\" type=\"sr_movements\">"
00132         string += "<remap from=\"~targets\" to=\""+ controller_name_ +"/command\"/>"
00133         string += "<remap from=\"~inputs\" to=\""+ controller_name_ +"/state\"/>"
00134         string += "<param name=\"image_path\" value=\"$(find sr_movements)/movements/test.png\"/>"
00135         string += "<param name=\"min\" value=\""+ str(min_max[0]) +"\"/>"
00136         string += "<param name=\"max\" value=\""+ str(min_max[1]) + "\"/>"
00137         string += "<param name=\"publish_rate\" value=\"100\"/>"
00138         string += "<param name=\"repetition\" value=\"1000\"/>"
00139         string += "<param name=\"nb_step\" value=\"10000\"/>"
00140         string += "<param name=\"msg_type\" value=\"sr\"/>"
00141         string += "</node> </launch>"
00142 
00143         tmp_launch_file = NamedTemporaryFile(delete=False)
00144 
00145         tmp_launch_file.writelines(string)
00146         tmp_launch_file.close()
00147 
00148         return tmp_launch_file.name
00149 
00150     def get_min_max_(self):
00151         if self.joint_name_ in ["FFJ0", "MFJ0", "RFJ0", "LFJ0"]:
00152             return [0.0, math.radians(180.0)]
00153         elif self.joint_name_ in ["FFJ3", "MFJ3", "RFJ3", "LFJ3", "THJ1"]:
00154             return [0.0, math.radians(90.0)]
00155         elif self.joint_name_ in ["LFJ5"]:
00156             return [0.0, math.radians(45.0)]
00157         elif self.joint_name_ in ["FFJ4", "MFJ4", "RFJ4", "LFJ4"]:
00158             return [math.radians(-25.0), math.radians(25.0)]
00159         elif self.joint_name_ in ["THJ2"]:
00160             return [math.radians(-30.0), math.radians(30.0)]
00161         elif self.joint_name_ in ["THJ3"]:
00162             return [math.radians(-15.0), math.radians(15.0)]
00163         elif self.joint_name_ in ["THJ4"]:
00164             return [math.radians(0.0), math.radians(70.0)]
00165         elif self.joint_name_ in ["THJ5"]:
00166             return [math.radians(-60.0), math.radians(60.0)]
00167         elif self.joint_name_ in ["WRJ1"]:
00168             return [math.radians(-35.0), math.radians(45.0)]
00169         elif self.joint_name_ in ["WRJ2"]:
00170             return [math.radians(-30.0), math.radians(10.0)]
00171 
00172     def launch_(self):
00173         filename = self.create_launch_file_()
00174 
00175         launch_string = "roslaunch "+filename
00176 
00177         self.subprocess_.append( subprocess.Popen(launch_string.split()) )
00178 
00179 class SrGuiControllerTuner(Plugin):
00180 
00181     def __init__(self, context):
00182         super(SrGuiControllerTuner, self).__init__(context)
00183         self.setObjectName('SrGuiControllerTuner')
00184 
00185         self.controller_type = None
00186 
00187         self._publisher = None
00188         self._widget = QWidget()
00189 
00190         self.file_to_save = None
00191 
00192         ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../uis/SrGuiControllerTuner.ui')
00193         loadUi(ui_file, self._widget)
00194         self._widget.setObjectName('SrControllerTunerUi')
00195         context.add_widget(self._widget)
00196 
00197         #stores the movements threads to be able to stop them
00198         self.move_threads = []
00199 
00200         #stores the controllers in the same order as the dropdown
00201         self.controllers_in_dropdown = []
00202 
00203         #stores a dictionary of the widgets containing the data for
00204         # the different controllers.
00205         self.ctrl_widgets = {}
00206 
00207         #a library which helps us doing the real work.
00208         self.sr_controller_tuner_app_ = SrControllerTunerApp( os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../data/controller_settings.xml') )
00209 
00210         #refresh the controllers once
00211         self.on_btn_refresh_ctrl_clicked_()
00212         #attach the button pressed to its action
00213         self._widget.btn_refresh_ctrl.pressed.connect(self.on_btn_refresh_ctrl_clicked_)
00214         self._widget.dropdown_ctrl.currentIndexChanged.connect(self.on_changed_controller_type_)
00215 
00216         self._widget.btn_save_selected.pressed.connect(self.on_btn_save_selected_clicked_)
00217         self._widget.btn_save_all.pressed.connect(self.on_btn_save_all_clicked_)
00218         self._widget.btn_select_file_path.pressed.connect(self.on_btn_select_file_path_clicked_)
00219 
00220         self._widget.btn_set_selected.pressed.connect(self.on_btn_set_selected_clicked_)
00221         self._widget.btn_set_all.pressed.connect(self.on_btn_set_all_clicked_)
00222 
00223         self._widget.btn_load.pressed.connect(self.on_btn_load_clicked_)
00224         self._widget.btn_stop_mvts.pressed.connect(self.on_btn_stop_mvts_clicked_)
00225 
00226     def on_btn_plot_pressed_(self, joint_name, btn):
00227         plot_thread = PlotThread(btn, joint_name, self.controller_type)
00228         plot_thread.start()
00229 
00230     def on_btn_move_pressed_(self, joint_name, btn):
00231         move_thread = MoveThread(btn, joint_name, self.controller_type)
00232         move_thread.start()
00233         self.move_threads.append(move_thread)
00234 
00235     def on_changed_controller_type_(self, index = None):
00236         if index == None:
00237             return
00238         self.reset_file_path()
00239         self.refresh_controller_tree_( self.controllers_in_dropdown[index] )
00240         
00241     def reset_file_path(self):
00242         self._widget.txt_file_path.setText("")
00243 
00244         self._widget.btn_load.setEnabled(False)
00245 
00246         #disable the save buttons (until a new file has
00247         # been chosen by the user)
00248         self._widget.btn_save_all.setEnabled(False)
00249         self._widget.btn_save_selected.setEnabled(False)
00250 
00251     def on_btn_select_file_path_clicked_(self):
00252         path_to_config = "~"
00253         try:
00254             path_to_config = roslib.packages.get_pkg_dir("sr_ethercat_hand_config")
00255         except:
00256             rospy.logwarn("couldnt find the sr_ethercat_hand_config package, do you have the sr_config stack installed?")
00257 
00258         subpath = "/controls/host"
00259         if self.sr_controller_tuner_app_.edit_only_mode:
00260             filter_files = "*.yaml"
00261         else:
00262             if self.controller_type == "Motor Force":
00263                 filter_files = "*controllers.yaml"
00264             else:
00265                 if self.sr_controller_tuner_app_.control_mode == "PWM":
00266                     filter_files = "*s_PWM.yaml"
00267                 else:                    
00268                     filter_files = "*s.yaml"
00269                 
00270         if self.controller_type == "Motor Force":
00271             filter_files = "Config (*motor"+filter_files+")"
00272             subpath = "/controls/motors"
00273         elif self.controller_type == "Position":
00274             filter_files = "Config (*position_controller"+filter_files+")"
00275         elif self.controller_type == "Velocity":
00276             filter_files = "Config (*velocity_controller"+filter_files+")"
00277         elif self.controller_type == "Mixed Position/Velocity":
00278             filter_files = "Config (*position_velocity"+filter_files+")"
00279         elif self.controller_type == "Effort":
00280             filter_files = "Config (*effort_controller"+filter_files+")"
00281 
00282         path_to_config += subpath
00283 
00284         filename, _ = QFileDialog.getOpenFileName(self._widget.tree_ctrl_settings, self._widget.tr('Save Controller Settings'),
00285                                                   self._widget.tr(path_to_config),
00286                                                   self._widget.tr(filter_files))
00287 
00288         if filename == "":
00289             return
00290 
00291         self.file_to_save = filename
00292 
00293         self._widget.txt_file_path.setText(filename)
00294 
00295         self._widget.btn_load.setEnabled(True)
00296 
00297         #enable the save buttons once a file has
00298         # been chosen
00299         self._widget.btn_save_all.setEnabled(True)
00300         self._widget.btn_save_selected.setEnabled(True)
00301 
00302     def on_btn_load_clicked_(self):
00303         #reload the parameters in rosparam, then refresh the tree widget
00304         paramlist = rosparam.load_file( self.file_to_save )
00305         for params,ns in paramlist:
00306             rosparam.upload_params(ns, params)
00307 
00308         self.refresh_controller_tree_( self.controllers_in_dropdown[self._widget.dropdown_ctrl.currentIndex()] )
00309 
00310     def on_btn_save_selected_clicked_(self):
00311         selected_items = self._widget.tree_ctrl_settings.selectedItems()
00312 
00313         if len( selected_items ) == 0:
00314             QMessageBox.warning(self._widget.tree_ctrl_settings, "Warning", "No motors selected.")
00315 
00316         for it in selected_items:
00317             if str(it.text(1)) != "":
00318                 self.save_controller( str(it.text(1)) )
00319 
00320     def on_btn_save_all_clicked_(self):
00321         for motor in self.ctrl_widgets.keys():
00322             self.save_controller( motor )
00323 
00324     def on_btn_set_selected_clicked_(self):
00325         selected_items = self._widget.tree_ctrl_settings.selectedItems()
00326 
00327         if len( selected_items ) == 0:
00328             QMessageBox.warning(self._widget.tree_ctrl_settings, "Warning", "No motors selected.")
00329 
00330         for it in selected_items:
00331             if str(it.text(1)) != "":
00332                 self.set_controller( str(it.text(1)) )
00333 
00334     def on_btn_set_all_clicked_(self):
00335         for motor in self.ctrl_widgets.keys():
00336             self.set_controller( motor )
00337 
00338     def on_btn_refresh_ctrl_clicked_(self):
00339         ctrls = self.sr_controller_tuner_app_.get_ctrls()
00340         self.sr_controller_tuner_app_.refresh_control_mode()
00341         self.controllers_in_dropdown = []
00342         self._widget.dropdown_ctrl.clear()
00343         for ctrl in ctrls:
00344             self._widget.dropdown_ctrl.addItem(ctrl)
00345             self.controllers_in_dropdown.append(ctrl)
00346 
00347         self.refresh_controller_tree_()
00348 
00349     def read_settings(self, joint_name):
00350         dict_of_widgets = self.ctrl_widgets[joint_name]
00351 
00352         settings = {}
00353         for item in dict_of_widgets.items():
00354             if item[0] == "sign":
00355                 if item[1].checkState() == Qt.Checked:
00356                     settings["sign"] = 1
00357                 else:
00358                     settings["sign"] = 0
00359             else:
00360                 try:
00361                     settings[item[0]] = item[1].value()
00362                 except AttributeError:
00363                     pass
00364 
00365         return settings
00366 
00367     def set_controller(self, joint_name):
00368         """
00369         Sets the current values for the given controller
00370         using the ros service.
00371         """
00372         settings = self.read_settings( joint_name )
00373 
00374         #uses the library to call the service properly
00375         success = self.sr_controller_tuner_app_.set_controller(joint_name, self.controller_type, settings)
00376         if success == False:
00377             if self.controller_type == "Motor Force":
00378                 QMessageBox.warning(self._widget.tree_ctrl_settings, "Warning", "Failed to set the PID values for joint "+ joint_name +". This won't work for Gazebo controllers as there are no force controllers yet.")
00379             else:
00380                 QMessageBox.warning(self._widget.tree_ctrl_settings, "Warning", "Failed to set the PID values for joint "+ joint_name +".")
00381 
00382 
00383     def save_controller(self, joint_name):
00384         """
00385         Sets the current values for the given controller
00386         using the ros service.
00387         """
00388         settings = self.read_settings( joint_name )
00389 
00390         #uses the library to call the service properly
00391         self.sr_controller_tuner_app_.save_controller(joint_name, self.controller_type, settings, self.file_to_save)
00392 
00393 
00394     def refresh_controller_tree_(self, controller_type = "Motor Force"):
00395         """
00396         Get the controller settings and their ranges and display them in
00397         the tree.
00398         """
00399         #buttons "set all" "set selected" and "stop movements" are disabled in edit_only_mode
00400         if self.sr_controller_tuner_app_.edit_only_mode:
00401             self._widget.btn_set_selected.setEnabled(False)
00402             self._widget.btn_set_all.setEnabled(False)
00403             self._widget.btn_stop_mvts.setEnabled(False)
00404         else:
00405             self._widget.btn_set_selected.setEnabled(True)
00406             self._widget.btn_set_all.setEnabled(True)
00407             self._widget.btn_stop_mvts.setEnabled(True)
00408         
00409         self.controller_type = controller_type
00410         ctrl_settings = self.sr_controller_tuner_app_.get_controller_settings( controller_type )
00411 
00412         self._widget.tree_ctrl_settings.clear()
00413         self._widget.tree_ctrl_settings.setColumnCount(ctrl_settings.nb_columns)
00414 
00415         tmp_headers = []
00416         for header in ctrl_settings.headers:
00417             tmp_headers.append( header["name"] )
00418         self._widget.tree_ctrl_settings.setHeaderLabels( tmp_headers )
00419 
00420         hand_item = QTreeWidgetItem(ctrl_settings.hand_item)
00421         self._widget.tree_ctrl_settings.addTopLevelItem(hand_item)
00422         for index_finger,finger_settings in enumerate(ctrl_settings.fingers):
00423             finger_item = QTreeWidgetItem( hand_item, finger_settings )
00424             self._widget.tree_ctrl_settings.addTopLevelItem(finger_item)
00425             for motor_settings in ctrl_settings.motors[index_finger]:
00426                 motor_name = motor_settings[1]
00427 
00428                 motor_item = QTreeWidgetItem( finger_item, motor_settings )
00429                 self._widget.tree_ctrl_settings.addTopLevelItem(motor_item)
00430 
00431                 parameter_values = self.sr_controller_tuner_app_.load_parameters( controller_type, motor_name )
00432                 if parameter_values != -1:
00433                     #the parameters have been found
00434                     self.ctrl_widgets[ motor_name ] = {}
00435 
00436                     #buttons for plot/move are not added in edit_only_mode
00437                     if not self.sr_controller_tuner_app_.edit_only_mode:
00438                         #add buttons for the automatic procedures (plot / move / ...)
00439                         frame_buttons = QFrame()
00440                         layout_buttons = QHBoxLayout()
00441                         btn_plot = QPushButton("Plot")
00442                         self.ctrl_widgets[ motor_name ]["btn_plot"] = btn_plot
00443                         self.ctrl_widgets[ motor_name ]["btn_plot"].clicked.connect(partial(self.on_btn_plot_pressed_, motor_name, self.ctrl_widgets[ motor_name ]["btn_plot"]))
00444                         layout_buttons.addWidget(btn_plot)
00445     
00446                         if self.controller_type in ["Position", "Mixed Position/Velocity"]:
00447                             #only adding Move button for position controllers
00448                             btn_move = QPushButton("Move")
00449                             self.ctrl_widgets[ motor_name ]["btn_move"] = btn_move
00450                             self.ctrl_widgets[ motor_name ]["btn_move"].clicked.connect(partial(self.on_btn_move_pressed_,motor_name, self.ctrl_widgets[ motor_name ]["btn_move"]))
00451                             layout_buttons.addWidget(btn_move)
00452                             frame_buttons.setLayout(layout_buttons)
00453     
00454                         self._widget.tree_ctrl_settings.setItemWidget(  motor_item, 0, frame_buttons )
00455 
00456                     for index_item,item in enumerate(ctrl_settings.headers):
00457                         if item["type"] == "Bool":
00458                             check_box = QCheckBox()
00459 
00460                             if parameter_values["sign"] == 1.0:
00461                                 check_box.setChecked(True)
00462 
00463                             check_box.setToolTip("Check if you want a negative sign\n(if the motor is being driven\n the wrong way around).")
00464                             self._widget.tree_ctrl_settings.setItemWidget(  motor_item, index_item, check_box )
00465 
00466                             self.ctrl_widgets[ motor_name ]["sign"] = check_box
00467 
00468 
00469                         if item["type"] == "Int":
00470                             spin_box = QSpinBox()
00471                             spin_box.setRange(int( item["min"] ), int( item["max"] ))
00472 
00473                             param_name = item["name"].lower()
00474                             spin_box.setValue( int(parameter_values[ param_name ] ) )
00475                             self.ctrl_widgets[ motor_name ][param_name] = spin_box
00476 
00477                             self._widget.tree_ctrl_settings.setItemWidget(  motor_item, index_item, spin_box )
00478 
00479                         if item["type"] == "Float":
00480                             spin_box = QDoubleSpinBox()
00481                             spin_box.setRange( -65535.0, 65535.0 )
00482                             spin_box.setDecimals(3)
00483 
00484                             param_name = item["name"].lower()
00485                             spin_box.setValue(float(parameter_values[param_name]))
00486                             self.ctrl_widgets[ motor_name ][param_name] = spin_box
00487 
00488                             self._widget.tree_ctrl_settings.setItemWidget(  motor_item, index_item, spin_box )
00489 
00490                         motor_item.setExpanded(True)
00491                 else:
00492                     motor_item.setText(1, "parameters not found - controller tuning disabled")
00493             finger_item.setExpanded(True)
00494         hand_item.setExpanded(True)
00495 
00496         for col in range(0, self._widget.tree_ctrl_settings.columnCount()):
00497             self._widget.tree_ctrl_settings.resizeColumnToContents(col)
00498 
00499 
00500     def on_btn_stop_mvts_clicked_(self):
00501         for move_thread in self.move_threads:
00502             move_thread.__del__()
00503         self.move_threads = []
00504 
00505 
00506     #########
00507     #Default methods for the rosgui plugins
00508 
00509     def _unregisterPublisher(self):
00510         if self._publisher is not None:
00511             self._publisher.unregister()
00512             self._publisher = None
00513 
00514     def shutdown_plugin(self):
00515         self._unregisterPublisher()
00516 
00517     def save_settings(self, global_settings, perspective_settings):
00518         pass
00519 
00520     def restore_settings(self, global_settings, perspective_settings):
00521         pass
00522 


sr_gui_controller_tuner
Author(s): Ugo Cupcic
autogenerated on Fri Jan 3 2014 12:04:50