manager.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Software License Agreement (BSD License)
00004 #
00005 # Copyright (c) 2009, Willow Garage, Inc.
00006 # All rights reserved.
00007 #
00008 # Redistribution and use in source and binary forms, with or without
00009 # modification, are permitted provided that the following conditions
00010 # are met:
00011 #
00012 #  * Redistributions of source code must retain the above copyright
00013 #    notice, this list of conditions and the following disclaimer.
00014 #  * Redistributions in binary form must reproduce the above
00015 #    copyright notice, this list of conditions and the following
00016 #    disclaimer in the documentation and/or other materials provided
00017 #    with the distribution.
00018 #  * Neither the name of the Willow Garage nor the names of its
00019 #    contributors may be used to endorse or promote products derived
00020 #    from this software without specific prior written permission.
00021 #
00022 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00023 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00024 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00025 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00026 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00027 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00028 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00029 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00030 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00031 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00032 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00033 # POSSIBILITY OF SUCH DAMAGE.
00034 
00035 ##\author Kevin Watts
00036 ##\brief Test Manager main GUI/starting point
00037 
00038 from __future__ import with_statement
00039 PKG = 'life_test'
00040 import roslib
00041 roslib.load_manifest(PKG)
00042 
00043 import rospy
00044 
00045 import socket
00046 import os
00047 import sys
00048 import datetime
00049 import wx
00050 import time
00051 import traceback
00052 import threading
00053 from wx import xrc
00054 from wx import html
00055 
00056 import threading
00057 
00058 # Package imports
00059 from test_monitor_panel import TestMonitorPanel
00060 from test_param import *
00061 from test_bay import *
00062 from config_loader import * 
00063 
00064 # Messages / Services
00065 from pr2_self_test_msgs.msg import TestInfoArray
00066 from pr2_power_board.srv import PowerBoardCommand
00067 from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue
00068 from std_msgs.msg import Empty
00069 
00070 from roslaunch_caller import roslaunch_caller 
00071 from wg_invent_client import Invent
00072 
00073 class TestManagerFrame(wx.Frame):
00074     def __init__(self, parent, debug = False):
00075         wx.Frame.__init__(self, parent, wx.ID_ANY, "Test Manager")
00076         
00077         self._mutex = threading.Lock()
00078 
00079         self.create_menu_bar()
00080 
00081         self._debug = debug
00082 
00083         # Load tests by short serial number
00084         self._tests_by_serial = {}
00085 
00086         # Load XRC
00087         xrc_path = os.path.join(roslib.packages.get_pkg_dir('life_test'), 'xrc/gui.xrc')
00088         self._xrc = xrc.XmlResource(xrc_path)
00089 
00090         # Allow roslaunch to ssh into unknown machines
00091         os.environ['ROSLAUNCH_SSH_UNKNOWN'] = '1'
00092 
00093         # Get real username/password for invent here...
00094         self._invent_client = None 
00095         
00096         self._active_bays = []
00097         self._active_boards = {}
00098 
00099         self.load_tests_from_file()
00100         self.load_rooms_from_file()
00101 
00102         self._main_panel = self._xrc.LoadPanel(self, 'manager_panel')
00103 
00104         self._tab_ctrl = xrc.XRCCTRL(self._main_panel, 'test_tab_control')
00105         self._start_panel = xrc.XRCCTRL(self._tab_ctrl, 'start_panel')
00106         self._serial_text = xrc.XRCCTRL(self._start_panel, 'serial_text')
00107         self._start_button = xrc.XRCCTRL(self._start_panel, 'start_button')
00108         self._start_button.Bind(wx.EVT_BUTTON, self.on_start)
00109 
00110         self._log_text = xrc.XRCCTRL(self._start_panel, 'log_text')
00111 
00112         self.Bind(wx.EVT_CLOSE, self.on_close)
00113         self._serial_text.SetFocus()
00114         
00115         # Start panel...
00116         self._test_panels = {}
00117         self._active_serials = []
00118 
00119         self._current_log = []
00120 
00121         self._diag_sub = rospy.Subscriber('/diagnostics', DiagnosticArray, self._diag_cb)
00122         self._diags = []
00123 
00124         self._power_node = None
00125         self._power_cmd = rospy.ServiceProxy('power_board/control', PowerBoardCommand)
00126 
00127         self.log('Started Test Manager')
00128 
00129         self._info_pub = rospy.Publisher('test_info', TestInfoArray)
00130 
00131         self._heartbeat_pub = rospy.Publisher('/heartbeat', Empty)
00132 
00133         self._info_timer = wx.Timer(self, 1)
00134         self.Bind(wx.EVT_TIMER, self.on_info_timer, self._info_timer)
00135         self._info_timer.Start(500)
00136 
00137         self._heartbeat_timer = wx.Timer()
00138         self._heartbeat_timer.Bind(wx.EVT_TIMER, self.on_heartbeat_timer)
00139         self._heartbeat_timer.Start(1000)
00140 
00141     def shutdown(self):
00142         if self._power_node is not None:
00143             self._power_node.shutdown()
00144             self._power_node = None
00145         
00146     def __del__(self):
00147         self.shutdown()
00148 
00149     @property
00150     def invent_client(self):
00151         return self._invent_client
00152 
00153     def on_info_timer(self, event):
00154         array = TestInfoArray()
00155         for serial, panel in self._test_panels.iteritems():
00156             array.data.append(panel.on_status_check())
00157         self._info_pub.publish(array)
00158 
00159     def on_heartbeat_timer(self, event):
00160         beat = Empty()
00161         self._heartbeat_pub.publish(beat)
00162 
00163     def _diag_cb(self, msg):
00164         with self._mutex:
00165             self._diags.append(msg)
00166 
00167         wx.CallAfter(self._new_diag)
00168         
00169     ##\brief Look for known power boards in diagnostics, update panels
00170     ##
00171     def _new_diag(self):
00172         with self._mutex:
00173             # Search for power board messages. If we have them, update the status in the viewer
00174             for msg in self._diags:
00175                 for status in msg.status:
00176                     if status.name.startswith("Power board"):
00177                         board_sn = int(status.name.split()[2])
00178                         if self._active_boards.has_key(board_sn):
00179                             runstop = False
00180                             runbutton = False
00181                             for val in status.values:
00182                                 if val.key == "RunStop Button Status":
00183                                     runbutton = (val.value == "True")
00184                                 if val.key == "RunStop Status":
00185                                     runstop = (val.value == "True")
00186                             estop = runstop and runbutton
00187                             
00188                             for val in status.values:
00189                                 if val.key.startswith("Breaker "):
00190                                     for breaker in self._active_boards[board_sn].keys():
00191                                         if val.key == "Breaker %d State" % breaker:
00192                                             panel = self._active_boards[board_sn][breaker]
00193                                             self._test_panels[panel].update_board(val.value, estop)
00194 
00195 
00196         # Clear diagnostics data
00197         self._diags = []
00198 
00199 
00200 
00201                 
00202 
00203     def load_test(self, test, serial):
00204         # Make notebook page for panel
00205         # Add panel to notebook
00206         panel = wx.Panel(self._tab_ctrl, wx.ID_ANY)
00207 
00208         self._tab_ctrl.AddPage(panel, test.get_title(serial), True)
00209 
00210         test_panel = TestMonitorPanel(panel, self, test, serial)
00211         sizer = wx.BoxSizer(wx.VERTICAL)
00212         sizer.Add(test_panel, 1, wx.EXPAND)
00213         panel.SetSizer(sizer)
00214         panel.Fit()
00215         panel.Layout()
00216         
00217         self._test_panels[serial] = test_panel        
00218         self._active_serials.append(serial)
00219 
00220     def close_tab(self, serial):
00221         if not serial in self._active_serials:
00222             rospy.logerr('Requested serial not in active serials: %s' % serial)
00223             return
00224 
00225         idx = self._active_serials.index(serial)
00226 
00227         if self._tab_ctrl.DeletePage(idx + 1):
00228             del self._test_panels[serial]
00229             del self._active_serials[idx]
00230 
00231     def test_start_check(self, bay, serial):
00232         if bay in self._active_bays:
00233             return False
00234 
00235         self._active_bays.append(bay)
00236 
00237         if bay.board is not None:
00238             self._active_boards.setdefault(bay.board, {})[bay.breaker] = serial
00239 
00240         if bay.board is not None and self._power_node is None:
00241             self._power_node = roslaunch_caller.ScriptRoslaunch('<launch><node pkg="pr2_power_board" type="power_node" name="power_board" respawn="true" /></launch>')
00242             self._power_node.start()
00243 
00244         return True
00245 
00246     def test_stop(self, bay):
00247         if bay in self._active_bays:
00248             idx = self._active_bays.index(bay)
00249             del self._active_bays[idx]
00250 
00251         if bay.board is not None:
00252             del self._active_boards[bay.board][bay.breaker]
00253             if len(self._active_boards[bay.board].keys()) == 0:
00254                 del self._active_boards[bay.board]
00255 
00256         if len(self._active_boards.keys()) == 0 and self._power_node is not None:
00257             self._power_node.shutdown()
00258             self._power_node = None
00259 
00260     # Power commands
00261     def _reset_power_disable(self, bay):
00262         try:
00263             rospy.wait_for_service('power_board/control', 5)
00264             resp = self._power_cmd(bay.board, bay.breaker, 'reset', 0)
00265             if resp.retval != 0:
00266                 rospy.logerr('Failed to reset board %s, breaker %d. Retval: %s' % (bay.board, bay.breaker, resp.retval))
00267                 return False
00268 
00269             time.sleep(1)
00270             return True
00271         except:
00272             rospy.logerr(traceback.format_exc())
00273             return False
00274 
00275 
00276     def power_run(self, bay):
00277         try:
00278             if not self._reset_power_disable(bay):
00279                 return False
00280 
00281             resp = self._power_cmd(bay.board, bay.breaker, 'start', 0)
00282             return resp.retval == 0
00283         except:
00284             rospy.logerr(traceback.format_exc())
00285             return False
00286 
00287     def power_standby(self, bay):
00288         try:
00289             if not self._reset_power_disable(bay):
00290                 return False
00291 
00292             resp = self._power_cmd(bay.board, bay.breaker, 'stop', 0)
00293             return resp.retval == 0
00294         except:
00295             return False
00296 
00297     def power_disable(self, bay):
00298         try:
00299             rospy.wait_for_service('power_board/control', 5)
00300             resp = self._power_cmd(bay.board, bay.breaker, 'disable', 0)
00301             return resp.retval == 0
00302         except:
00303             return False
00304              
00305     def load_invent_client(self):
00306         # Loads invent client. 
00307         # Like singleton, doesn't construct unless valid login
00308         if self._invent_client:
00309             return True
00310 
00311         invent = None
00312         dialog = self._xrc.LoadDialog(self, 'username_password_dialog')
00313         xrc.XRCCTRL(dialog, 'login_text').Wrap(300)
00314         dialog.Layout()
00315         dialog.Fit()
00316         username_ctrl = xrc.XRCCTRL(dialog, 'username')
00317         password_ctrl = xrc.XRCCTRL(dialog, 'password')
00318         username_ctrl.SetFocus()
00319         
00320         username_ctrl.SetMinSize(wx.Size(200, -1))
00321         password_ctrl.SetMinSize(wx.Size(200, -1))
00322 
00323         while not rospy.is_shutdown():
00324             if (dialog.ShowModal() == wx.ID_OK):
00325                 username = username_ctrl.GetValue()
00326                 password = password_ctrl.GetValue()
00327             
00328                 invent = Invent(username, password)
00329                 if (invent.login() == False):
00330                     wx.MessageBox('Please enter a valid username and password.',
00331                                   'Valid Login Required', wx.OK|wx.ICON_ERROR, 
00332                                   self)
00333                 else:
00334                     self._invent_client = invent
00335                     return True
00336             else:
00337                 return False # User doesn't want to log in
00338 
00339         return False
00340       
00341  
00342     def on_start(self, event):
00343         serial = self._serial_text.GetValue()
00344         
00345         if serial in self._active_serials:
00346             wx.MessageBox('Current component is already testing.', 'Already Testing', wx.OK|wx.ICON_ERROR, self)
00347             return
00348 
00349         if not self._debug and not self.load_invent_client():
00350             wx.MessageBox('You cannot proceed without a valid inventory login.', 'Valid Login Required', wx.OK|wx.ICON_ERROR, self)
00351             return 
00352         elif self._debug and not self.load_invent_client():
00353             wx.MessageBox('Warning: Without inventory access, a lot of things won\'t really work.',
00354                           'No Inventory Access', wx.OK|wx.ICON_ERROR, self)
00355             self._invent_client = None
00356             
00357         if (not self._debug) and (not self._invent_client.check_serial_valid(serial)):
00358             wx.MessageBox('Serial number "%s" appears to be invalid. Re-enter the serial number and try again.' % (serial), 'Invalid Serial Number', wx.OK|wx.ICON_ERROR, self)
00359             return
00360 
00361         if (not self._debug) and (not self._invent_client.get_test_status(serial)):
00362             wx.MessageBox('Component %s has not passed qualification. Please re-test component and try again' % serial, 
00363                           'Bad Component', wx.OK|wx.ICON_ERROR, self)
00364             return
00365 
00366 
00367 
00368         test = self.select_test(serial)
00369 
00370         # If test is none, display message box and return
00371         if not test:
00372             wx.MessageBox('No test defined for that serial number or no test selected. Please try again.', 'No test', wx.OK|wx.ICON_ERROR, self)
00373             return
00374         
00375         self._serial_text.Clear()
00376         self.log('Starting test %s' % test._name)
00377 
00378         self.load_test(test, serial)
00379 
00380     ## Loads locations for tests
00381     def load_rooms_from_file(self):
00382         my_rooms = load_rooms_from_file()
00383         if len(my_rooms.items()) == 0:
00384             wx.MessageBox('Unable to load test rooms and bays information from rooms file. Check the file and try again',
00385                           'Unable to load rooms', wx.OK|wx.ICON_ERROR, self)
00386             return
00387         self._rooms = my_rooms
00388 
00389         if self._rooms.has_key(socket.gethostname()):
00390             self.room = self._rooms[socket.gethostname()]
00391         else:
00392             self.room = TestRoom(socket.gethostname())
00393         
00394 
00395     # Loads tests from XML file
00396     def load_tests_from_file(self, test_xml_path = os.path.join(roslib.packages.get_pkg_dir('life_test'), 'tests.xml')):
00397         my_tests = load_tests_from_file(test_xml_path)
00398 
00399         if len(my_tests.items()) == 0:
00400             wx.MessageBox('Unable to load test from file %s. Check the file and try again.' % test_xml_path,
00401                           'Unable to load', wx.OK|wx.ICON_ERROR, self)
00402             return
00403         self._tests = my_tests
00404         
00405 
00406     ##\brief Pop-up GUI that lets users select which test to load
00407     def select_string_from_list(self, msg, lst):
00408         # Load robot selection dialog
00409         dialog = self._xrc.LoadDialog(self, 'select_test_dialog')
00410         select_text = xrc.XRCCTRL(dialog, 'select_text')
00411         select_text.SetLabel(msg)
00412         test_box = xrc.XRCCTRL(dialog, 'test_list_box')
00413         test_box.InsertItems(lst, 0)
00414         
00415         select_text.Wrap(250)
00416         
00417         dialog.Layout()
00418         dialog.Fit()
00419         
00420         # return string of test folder/file to run
00421         if (dialog.ShowModal() == wx.ID_OK):
00422             desc = test_box.GetStringSelection()
00423             dialog.Destroy()
00424             return desc
00425         else: 
00426             dialog.Destroy()
00427             return None
00428 
00429     def select_test(self, serial):
00430         short_serial = serial[0:7]
00431         
00432         if not self._tests.has_key(short_serial):
00433             return None
00434         if len(self._tests[short_serial]) == 1:
00435             return self._tests[short_serial][0]
00436         
00437         # Load select_test_dialog
00438         tests_by_name = {}
00439         for test in self._tests[short_serial]:
00440             tests_by_name[test._name] = test
00441       
00442         msg = 'Select test to run.'
00443 
00444         descrips = dict.keys(tests_by_name)
00445         descrips.sort()
00446         
00447         choice = self.select_string_from_list(msg, descrips)
00448         if choice is None:
00449             return None
00450         return tests_by_name[choice]
00451 
00452     def log(self, msg):
00453         time_str = time.strftime("%m/%d/%Y %H:%M:%S: ", time.localtime(rospy.get_time()))
00454 
00455         self._current_log.append(time_str + msg)
00456         self.update_log_display()
00457 
00458     def log_test_entry(self, test_name, machine, message):
00459         if not machine:
00460             machine = 'NONE'
00461 
00462         time_str = time.strftime("%m/%d/%Y %H:%M:%S: ", time.localtime(rospy.get_time()))
00463 
00464         log_msg = time_str + 'Machine %s, Test %s. Message: %s' % (machine, test_name, message)
00465 
00466         self._current_log.append(log_msg)
00467         self.update_log_display()
00468 
00469     def update_log_display(self):
00470         for log in self._current_log:
00471             self._log_text.AppendText(log + '\n')
00472         self._current_log = []
00473 
00474     def on_close(self, event):
00475         if event.CanVeto() and len(self._test_panels) > 0:
00476                 wx.MessageBox('Unable to close Test Manager. All component windows must be shut down first.', 
00477                               'Unable to close', wx.OK|wx.ICON_ERROR, self)
00478                 event.Veto()
00479                 return
00480 
00481         # Could just delete monitors
00482         for value in dict.values(self._test_panels):
00483             value.on_close(None)
00484             #value.shutdown()
00485 
00486         self.Destroy()
00487         
00488     ## Add stuff for invent login, loading test bays
00489     def on_menu(self, event):
00490         if (event.GetEventObject() == self._file_menu):
00491             if (event.GetId() == wx.ID_EXIT):
00492                 self.Close()
00493                 return
00494             if (event.GetId() == 1001):
00495                 self._invent_client = None
00496                 self.load_invent_client()
00497                 return
00498 
00499         if (event.GetEventObject() == self._tests_menu):
00500             if (event.GetId() == 2001):
00501                 self.load_tests_from_file()
00502                 return
00503             if (event.GetId() == 2002):
00504                 self.load_rooms_from_file()
00505                 return
00506 
00507     def create_menu_bar(self):
00508         menubar = wx.MenuBar()
00509         
00510         # file menu
00511         self._file_menu = wx.Menu()
00512         self._file_menu.Append(1001, "Invent Login\tCTRL+l")
00513         self._file_menu.Append(wx.ID_EXIT, "E&xit")
00514         menubar.Append(self._file_menu, "&File")
00515                 
00516         self._tests_menu = wx.Menu()
00517         self._tests_menu.Append(2001, "Reload tests (tests.xml)")
00518         self._tests_menu.Append(2002, "Reload rooms and bays (wg_test_rooms.xml)")
00519         menubar.Append(self._tests_menu, "&Tests")
00520 
00521         self.SetMenuBar(menubar)
00522         self.Bind(wx.EVT_MENU, self.on_menu)
00523 
00524 
00525 
00526 


life_test
Author(s): Kevin Watts
autogenerated on Sat Dec 28 2013 17:56:37