Package node_manager_fkie :: Module capability_table
[frames] | no frames]

Source Code for Module node_manager_fkie.capability_table

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2012, Fraunhofer FKIE/US, Alexander Tiderko 
  4  # All rights reserved. 
  5  # 
  6  # Redistribution and use in source and binary forms, with or without 
  7  # modification, are permitted provided that the following conditions 
  8  # are met: 
  9  # 
 10  #  * Redistributions of source code must retain the above copyright 
 11  #    notice, this list of conditions and the following disclaimer. 
 12  #  * Redistributions in binary form must reproduce the above 
 13  #    copyright notice, this list of conditions and the following 
 14  #    disclaimer in the documentation and/or other materials provided 
 15  #    with the distribution. 
 16  #  * Neither the name of Fraunhofer nor the names of its 
 17  #    contributors may be used to endorse or promote products derived 
 18  #    from this software without specific prior written permission. 
 19  # 
 20  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 21  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 22  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 23  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 24  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 25  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 26  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 27  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 28  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 29  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 30  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 31  # POSSIBILITY OF SUCH DAMAGE. 
 32   
 33  import os 
 34  import sys 
 35   
 36  from python_qt_binding import QtCore 
 37  from python_qt_binding import QtGui 
 38   
 39  import roslib 
 40  import rospy 
 41  import node_manager_fkie as nm 
 42  from master_discovery_fkie.master_info import NodeInfo  
 43   
 44   
 45  ################################################################################ 
 46  ##############                  CapabilityHeader                  ############## 
 47  ################################################################################ 
 48   
49 -class CapabilityHeader(QtGui.QHeaderView):
50 ''' 51 This class is used for visualization of robots or capabilities in header of 52 the capability table. It is also used to manage the displayed robots or 53 capabilities. Furthermore L{QtGui.QHeaderView.paintSection()} method is 54 overridden to paint the images in background of the cell. 55 ''' 56 57 description_requested_signal = QtCore.Signal(str, str) 58 '''the signal is emitted by click on a header to show a description.''' 59
60 - def __init__(self, orientation, parent=None):
61 QtGui.QHeaderView.__init__(self, orientation, parent) 62 self._data = [] 63 ''' @ivar a list with dictionaries C{dict('cfgs' : [], 'name': str, 'displayed_name' : str, 'type' : str, 'description' : str, 'images' : [QtGui.QPixmap])}''' 64 if orientation == QtCore.Qt.Horizontal: 65 self.setDefaultAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) 66 elif orientation == QtCore.Qt.Vertical: 67 self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
68
69 - def index(self, name):
70 ''' 71 Returns the index of the object stored with given name 72 @param name: the name of the item 73 @type name: C{str} 74 @return: the index or -1 if the item was not found 75 @rtype: C{int} 76 ''' 77 for index, entry in enumerate(self._data): 78 if entry['name'] == name: 79 return index 80 return -1
81
82 - def paintSection(self, painter, rect, logicalIndex):
83 ''' 84 The method paint the robot or capability images in the backgroud of the cell. 85 @see: L{QtGui.QHeaderView.paintSection()} 86 ''' 87 painter.save() 88 QtGui.QHeaderView.paintSection(self, painter, rect, logicalIndex) 89 painter.restore() 90 91 if logicalIndex in range(len(self._data)) and self._data[logicalIndex]['images']: 92 if len(self._data[logicalIndex]['images']) == 1: 93 pix = self._data[logicalIndex]['images'][0] 94 pix = pix.scaled(rect.width(), rect.height()-20, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) 95 self.style().drawItemPixmap(painter, rect, 5, pix) 96 elif len(self._data[logicalIndex]['images']) > 1: 97 new_rect = QtCore.QRect(rect.left(), rect.top(), rect.width(), (rect.height()-20) / 2.) 98 pix = self._data[logicalIndex]['images'][0] 99 pix = pix.scaled(new_rect.width(), new_rect.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) 100 self.style().drawItemPixmap(painter, new_rect, 5, pix) 101 new_rect = QtCore.QRect(rect.left(), rect.top()+new_rect.height(), rect.width(), new_rect.height()) 102 pix = self._data[logicalIndex]['images'][1] 103 pix = pix.scaled(new_rect.width(), new_rect.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) 104 self.style().drawItemPixmap(painter, new_rect, 5, pix)
105 106
107 - def mousePressEvent(self, event):
108 ''' 109 Interpret the mouse events to send the description of a robot or capability 110 if the user click on the header. 111 ''' 112 QtGui.QHeaderView.mousePressEvent(self, event) 113 index = self.logicalIndexAt(event.pos()) 114 if index in range(len(self._data)): 115 suffix = 'Capability' 116 if self.orientation() == QtCore.Qt.Horizontal: 117 suffix = 'Robot' 118 title = ' - '.join([self._data[index]['name'], suffix]) 119 text = self._data[index]['description'] 120 try: 121 from docutils import examples 122 text = examples.html_body(text) 123 except: 124 import traceback 125 rospy.logwarn("Error while generate description for %s: %s", self._data[index]['name'], traceback.format_exc()) 126 self.description_requested_signal.emit(title, text)
127
128 - def setDescription(self, index, cfg, name, displayed_name, type, description, images):
129 ''' 130 Sets the values of an existing item to the given items. 131 ''' 132 if index < len(self._data): 133 obj = self._data[index] 134 if not cfg in obj['cfgs']: 135 obj['cfgs'].append(cfg) 136 obj['name'] = name 137 obj['displayed_name'] = displayed_name 138 obj['type'] = type 139 obj['description'] = description 140 del obj['images'][:] 141 for image_path in images: 142 img = os.path.join(nm.settings().PACKAGE_DIR, image_path) 143 if os.path.isfile(img): 144 obj['images'].append(QtGui.QPixmap(img))
145
146 - def updateDescription(self, index, cfg, name, displayed_name, type, description, images):
147 ''' 148 Sets the values of an existing item to the given items only if the current 149 value is empty. 150 ''' 151 if index < len(self._data): 152 obj = self._data[index] 153 if not cfg in obj['cfgs']: 154 obj['cfgs'].append(cfg) 155 if not obj['name']: 156 obj['name'] = name 157 if not obj['displayed_name']: 158 obj['displayed_name'] = displayed_name 159 if not obj['type']: 160 obj['type'] = type 161 if not obj['description']: 162 obj['description'] = description 163 if not obj['images']: 164 for image_path in images: 165 img = os.path.join(nm.settings().PACKAGE_DIR, image_path) 166 if os.path.isfile(img): 167 obj['images'].append(QtGui.QPixmap(img))
168
169 - def removeDescription(self, index):
170 ''' 171 Removes an existing value from the header. 172 @param index: the index of the item to remove. 173 @type index: C{int} 174 ''' 175 if index < len(self._data): 176 self._data.pop(index)
177
178 - def insertItem(self, index):
179 ''' 180 Inserts an item at the given index into the header. 181 @param index: the index 182 @type index: C{int} 183 ''' 184 new_dict = {'cfgs' : [], 'name': '', 'displayed_name' : '', 'type' : '', 'description' : '', 'images' : []} 185 if index < len(self._data): 186 self._data.insert(index, new_dict) 187 else: 188 self._data.append(new_dict)
189
190 - def insertSortedItem(self, name, displayed_name):
191 ''' 192 Insert the new item with given name at the sorted position and return the index of 193 the item. 194 @param name: the name of the new item 195 @type name: C{str} 196 @return: index of the inserted item 197 @rtype: C{int} 198 ''' 199 new_dict = {'cfgs' : [], 'name': name, 'displayed_name' : displayed_name, 'type' : '', 'description' : '', 'images' : []} 200 for index, item in enumerate(self._data): 201 if item['displayed_name'].lower() > displayed_name.lower(): 202 self._data.insert(index, new_dict) 203 return index 204 self._data.append(new_dict) 205 return len(self._data)-1
206
207 - def removeCfg(self, cfg):
208 ''' 209 Removes the configuration entries from objects and returns the list with 210 indexes, where the configuration was removed. 211 @param cfg: configuration to remove 212 @type cfg: C{str} 213 @return: the list the indexes, where the configuration was removed 214 @rtype: C{[int]} 215 ''' 216 result = [] 217 for index, d in enumerate(self._data): 218 if cfg in d['cfgs']: 219 d['cfgs'].remove(cfg) 220 result.append(index) 221 return result
222
223 - def count(self):
224 ''' 225 @return: The count of items in the header. 226 @rtype: C{int} 227 ''' 228 return len(self._data)
229
230 - def getConfigs(self, index):
231 ''' 232 @return: The configurations assigned to the item at the given index 233 @rtype: C{str} 234 ''' 235 result = [] 236 if index < len(self._data): 237 result = list(self._data[index]['cfgs']) 238 return result
239 240 241 ################################################################################ 242 ############## CapabilityControlWidget ############## 243 ################################################################################ 244
245 -class CapabilityControlWidget(QtGui.QFrame):
246 ''' 247 The control widget contains buttons for control a capability. Currently this 248 are C{On} and C{Off} buttons. Additionally, the state of the capability is 249 color coded. 250 ''' 251 252 start_nodes_signal = QtCore.Signal(str, str, list) 253 '''@ivar: the signal is emitted to start on host(described by masteruri) the nodes described in the list, Parameter(masteruri, config, nodes).''' 254 255 stop_nodes_signal = QtCore.Signal(str, list) 256 '''@ivar: the signal is emitted to stop on masteruri the nodes described in the list.''' 257
258 - def __init__(self, masteruri, cfg, nodes, parent=None):
259 QtGui.QFrame.__init__(self, parent) 260 self._masteruri = masteruri 261 self._nodes = nodes 262 self._cfg = cfg 263 frame_layout = QtGui.QHBoxLayout(self) 264 frame_layout.setContentsMargins(0, 0, 0, 0) 265 frame_layout.addItem(QtGui.QSpacerItem(20, 20)) 266 self.on_button = QtGui.QPushButton() 267 self.on_button.setFlat(False) 268 self.on_button.setText("On") 269 self.on_button.clicked.connect(self.on_on_clicked) 270 frame_layout.addWidget(self.on_button) 271 272 self.off_button = QtGui.QPushButton() 273 self.off_button.setFlat(True) 274 self.off_button.setText("Off") 275 self.off_button.clicked.connect(self.on_off_clicked) 276 frame_layout.addWidget(self.off_button) 277 frame_layout.addItem(QtGui.QSpacerItem(20, 20))
278
279 - def config(self):
280 ''' 281 @return: the configuration defines this capability 282 @rtype: C{str} 283 ''' 284 return self._cfg
285
286 - def nodes(self):
287 ''' 288 @return: the list with nodes required by this capability. The nodes are 289 defined by ROS full name. 290 @rtype: C{[str]} 291 ''' 292 return self._nodes
293
294 - def setNodeState(self, running_nodes, stopped_nodes, error_nodes):
295 ''' 296 Sets the state of this capability. 297 @param running_nodes: a list with running nodes. 298 @type running_nodes: C{[str]} 299 @param stopped_nodes: a list with not running nodes. 300 @type stopped_nodes: C{[str]} 301 @param error_nodes: a list with nodes having a problem. 302 @type error_nodes: C{[str]} 303 ''' 304 self.setAutoFillBackground(True) 305 self.setBackgroundRole(QtGui.QPalette.Base) 306 palette = QtGui.QPalette() 307 if error_nodes: 308 brush = QtGui.QBrush(QtGui.QColor(255, 100, 0)) 309 elif running_nodes and stopped_nodes: 310 brush = QtGui.QBrush(QtGui.QColor(140, 185, 255)) #30, 50, 255 311 elif running_nodes: 312 self.on_button.setFlat(True) 313 self.off_button.setFlat(False) 314 brush = QtGui.QBrush(QtGui.QColor(59, 223, 18)) # 59, 223, 18 315 else: 316 brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 317 self.on_button.setFlat(False) 318 self.off_button.setFlat(True) 319 palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) 320 brush.setStyle(QtCore.Qt.SolidPattern) 321 palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) 322 self.setPalette(palette)
323 324
325 - def on_on_clicked(self):
326 self.start_nodes_signal.emit(self._masteruri, self._cfg, self._nodes) 327 self.on_button.setFlat(True) 328 self.off_button.setFlat(False)
329
330 - def on_off_clicked(self):
331 self.stop_nodes_signal.emit(self._masteruri, self._nodes) 332 self.on_button.setFlat(False) 333 self.off_button.setFlat(True)
334 335 336 337 ################################################################################ 338 ############## CapabilityTable ############## 339 ################################################################################ 340
341 -class CapabilityTable(QtGui.QTableWidget):
342 ''' 343 The table shows all detected capabilities of robots in tabular view. The 344 columns represents the robot and rows the capabilities. The cell of available 345 capability contains a L{CapabilityControlWidget} to show the state and manage 346 the capability. 347 ''' 348 349 start_nodes_signal = QtCore.Signal(str, str, list) 350 '''@ivar: the signal is emitted to start on host(described by masteruri) the nodes described in the list, Parameter(masteruri, config, nodes).''' 351 352 stop_nodes_signal = QtCore.Signal(str, list) 353 '''@ivar: the signal is emitted to stop on masteruri the nodes described in the list.''' 354 355 description_requested_signal = QtCore.Signal(str, str) 356 '''@ivar: the signal is emitted by click on a header to show a description.''' 357 358
359 - def __init__(self, parent=None):
360 QtGui.QTableWidget.__init__(self, parent) 361 self._robotHeader = CapabilityHeader(QtCore.Qt.Horizontal, self) 362 self._robotHeader.description_requested_signal.connect(self._show_description) 363 self.setHorizontalHeader(self._robotHeader) 364 self._capabilityHeader = CapabilityHeader(QtCore.Qt.Vertical, self) 365 self._capabilityHeader.description_requested_signal.connect(self._show_description) 366 self.setVerticalHeader(self._capabilityHeader)
367
368 - def updateCapabilities(self, masteruri, cfg_name, description):
369 ''' 370 Updates the capabilities view. 371 @param masteruri: the ROS master URI of updated ROS master. 372 @type masteruri: C{str} 373 @param cfg_name: The name of the node provided the capabilities description. 374 @type cfg_name: C{str} 375 @param description: The capabilities description object. 376 @type description: L{default_cfg_fkie.Description} 377 ''' 378 # if it is a new masteruri add a new column 379 robot_index = self._robotHeader.index(masteruri) 380 robot_name = description.robot_name if description.robot_name else nm.nameres().mastername(masteruri) 381 if robot_index == -1: 382 # append a new robot 383 robot_index = self._robotHeader.insertSortedItem(masteruri, robot_name) 384 self.insertColumn(robot_index) 385 # robot_index = self.columnCount()-1 386 # self._robotHeader.insertItem(robot_index) 387 self._robotHeader.setDescription(robot_index, cfg_name, masteruri, robot_name, description.robot_type, description.robot_descr.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), description.robot_images) 388 item = QtGui.QTableWidgetItem() 389 item.setSizeHint(QtCore.QSize(96,96)) 390 self.setHorizontalHeaderItem(robot_index, item) 391 self.horizontalHeaderItem(robot_index).setText(robot_name) 392 else: 393 #update 394 self._robotHeader.setDescription(robot_index, cfg_name, masteruri, robot_name, description.robot_type, description.robot_descr.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), description.robot_images) 395 396 #set the capabilities 397 for c in description.capabilities: 398 cap_index = self._capabilityHeader.index(c.name.decode(sys.getfilesystemencoding())) 399 if cap_index == -1: 400 # append a new capability 401 cap_index = self._capabilityHeader.insertSortedItem(c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding())) 402 self.insertRow(cap_index) 403 self.setRowHeight(cap_index, 96) 404 self._capabilityHeader.setDescription(cap_index, cfg_name, c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding()), c.type, c.description.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), c.images) 405 item = QtGui.QTableWidgetItem() 406 item.setSizeHint(QtCore.QSize(96,96)) 407 self.setVerticalHeaderItem(cap_index, item) 408 self.verticalHeaderItem(cap_index).setText(c.name.decode(sys.getfilesystemencoding())) 409 else: 410 self._capabilityHeader.updateDescription(cap_index, cfg_name, c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding()), c.type, c.description.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), c.images) 411 412 # add the capability control widget 413 controlWidget = CapabilityControlWidget(masteruri, cfg_name, c.nodes) 414 controlWidget.start_nodes_signal.connect(self._start_nodes) 415 controlWidget.stop_nodes_signal.connect(self._stop_nodes) 416 self.setCellWidget(cap_index, robot_index, controlWidget)
417
418 - def removeConfig(self, cfg):
419 ''' 420 @param cfg: The name of the node provided the capabilities description. 421 @type cfg: C{str} 422 ''' 423 removed_from_robots = self._robotHeader.removeCfg(cfg) 424 # for r in removed_from_robots: 425 # if not self._robotHeader.getConfigs(r): 426 # #remove the column with robot 427 # pass 428 removed_from_caps = self._capabilityHeader.removeCfg(cfg) 429 # remove control widget with given configuration 430 for r in reversed(removed_from_robots): 431 for c in removed_from_caps: 432 controlWidget = self.cellWidget(c, r) 433 if isinstance(controlWidget, CapabilityControlWidget) and controlWidget.config() == cfg: 434 self.removeCellWidget(c, r) 435 # remove empty columns 436 for r in removed_from_robots: 437 is_empty = True 438 for c in reversed(range(self.rowCount())): 439 controlWidget = self.cellWidget(c, r) 440 if isinstance(controlWidget, CapabilityControlWidget): 441 is_empty = False 442 break 443 if is_empty: 444 self.removeColumn(r) 445 self._robotHeader.removeDescription(r) 446 # remove empty rows 447 for c in reversed(removed_from_caps): 448 is_empty = True 449 for r in reversed(range(self.columnCount())): 450 controlWidget = self.cellWidget(c, r) 451 if isinstance(controlWidget, CapabilityControlWidget): 452 is_empty = False 453 break 454 if is_empty: 455 self.removeRow(c) 456 self._capabilityHeader.removeDescription(c)
457
458 - def updateState(self, masteruri, master_info):
459 ''' 460 Updates the run state of the capability. 461 @param masteruri: The ROS master, which sends the master_info 462 @type masteruri: C{str} 463 @param master_info: The state of the ROS master 464 @type master_info: L{master_discovery_fkie.MasterInfo} 465 ''' 466 if master_info is None or masteruri is None: 467 return 468 robot_index = self._robotHeader.index(masteruri) 469 if robot_index != -1: 470 for c in range(self.rowCount()): 471 controlWidget = self.cellWidget(c, robot_index) 472 if not controlWidget is None: 473 running_nodes = [] 474 stopped_nodes = [] 475 error_nodes = [] 476 for n in controlWidget.nodes(): 477 node = master_info.getNode(n) 478 if not node is None: 479 # while a synchronization there are node from other hosts in the master_info -> filter these nodes 480 if not node.uri is None and masteruri == node.masteruri: 481 if not node.pid is None: 482 running_nodes.append(n) 483 else: 484 error_nodes.append(n) 485 else: 486 stopped_nodes.append(n) 487 controlWidget.setNodeState(running_nodes, stopped_nodes, error_nodes)
488
489 - def _start_nodes(self, masteruri, cfg, nodes):
490 self.start_nodes_signal.emit(masteruri, cfg, nodes)
491
492 - def _stop_nodes(self, masteruri, nodes):
494
495 - def _show_description(self, name, description):
496 self.description_requested_signal.emit(name, description)
497