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 rospy 
 40  import node_manager_fkie as nm 
 41  from common import resolve_paths 
 42   
 43   
 44  ################################################################################ 
 45  ##############                  CapabilityHeader                  ############## 
 46  ################################################################################ 
 47   
48 -class CapabilityHeader(QtGui.QHeaderView):
49 ''' 50 This class is used for visualization of robots or capabilities in header of 51 the capability table. It is also used to manage the displayed robots or 52 capabilities. Furthermore L{QtGui.QHeaderView.paintSection()} method is 53 overridden to paint the images in background of the cell. 54 ''' 55 56 description_requested_signal = QtCore.Signal(str, str) 57 '''the signal is emitted by click on a header to show a description.''' 58
59 - def __init__(self, orientation, parent=None):
60 QtGui.QHeaderView.__init__(self, orientation, parent) 61 self._data = [] 62 ''' @ivar a list with dictionaries C{dict('cfgs' : [], 'name': str, 'displayed_name' : str, 'type' : str, 'description' : str, 'images' : [QtGui.QPixmap])}''' 63 if orientation == QtCore.Qt.Horizontal: 64 self.setDefaultAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) 65 elif orientation == QtCore.Qt.Vertical: 66 self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) 67 self.controlWidget = []
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 - def mousePressEvent(self, event):
107 ''' 108 Interpret the mouse events to send the description of a robot or capability 109 if the user click on the header. 110 ''' 111 QtGui.QHeaderView.mousePressEvent(self, event) 112 index = self.logicalIndexAt(event.pos()) 113 if index in range(len(self._data)): 114 suffix = 'Capability' 115 if self.orientation() == QtCore.Qt.Horizontal: 116 suffix = 'Robot' 117 title = ' - '.join([self._data[index]['name'], suffix]) 118 text = self._data[index]['description'] 119 try: 120 from docutils import examples 121 text = examples.html_body(text) 122 except: 123 import traceback 124 rospy.logwarn("Error while generate description for %s: %s", self._data[index]['name'], traceback.format_exc(1)) 125 self.description_requested_signal.emit(title, text)
126
127 - def setDescription(self, index, cfg, name, displayed_name, robot_type, description, images):
128 ''' 129 Sets the values of an existing item to the given items. 130 ''' 131 if index < len(self._data): 132 obj = self._data[index] 133 if not cfg in obj['cfgs']: 134 obj['cfgs'].append(cfg) 135 obj['name'] = name 136 obj['displayed_name'] = displayed_name 137 obj['type'] = robot_type 138 obj['description'] = resolve_paths(description) 139 del obj['images'][:] 140 for image_path in images: 141 img = resolve_paths(image_path) 142 if img and img[0] != os.path.sep: 143 img = os.path.join(nm.settings().PACKAGE_DIR, image_path) 144 if os.path.isfile(img): 145 obj['images'].append(QtGui.QPixmap(img))
146
147 - def updateDescription(self, index, cfg, name, displayed_name, robot_type, description, images):
148 ''' 149 Sets the values of an existing item to the given items only if the current 150 value is empty. 151 ''' 152 if index < len(self._data): 153 obj = self._data[index] 154 if not cfg in obj['cfgs']: 155 obj['cfgs'].append(cfg) 156 if not obj['name']: 157 obj['name'] = name 158 if not obj['displayed_name']: 159 obj['displayed_name'] = displayed_name 160 if not obj['type']: 161 obj['type'] = robot_type 162 if not obj['description']: 163 obj['description'] = resolve_paths(description) 164 if not obj['images']: 165 for image_path in images: 166 img = resolve_paths(image_path) 167 if img and img[0] != os.path.sep: 168 img = os.path.join(nm.settings().PACKAGE_DIR, image_path) 169 if os.path.isfile(img): 170 obj['images'].append(QtGui.QPixmap(img))
171
172 - def removeDescription(self, index):
173 ''' 174 Removes an existing value from the header. 175 @param index: the index of the item to remove. 176 @type index: C{int} 177 ''' 178 if index < len(self._data): 179 self._data.pop(index)
180
181 - def insertItem(self, index):
182 ''' 183 Inserts an item at the given index into the header. 184 @param index: the index 185 @type index: C{int} 186 ''' 187 new_dict = {'cfgs' : [], 'name': '', 'displayed_name' : '', 'type' : '', 'description' : '', 'images' : []} 188 if index < len(self._data): 189 self._data.insert(index, new_dict) 190 else: 191 self._data.append(new_dict)
192
193 - def insertSortedItem(self, name, displayed_name):
194 ''' 195 Insert the new item with given name at the sorted position and return the index of 196 the item. 197 @param name: the name of the new item 198 @type name: C{str} 199 @return: index of the inserted item 200 @rtype: C{int} 201 ''' 202 new_dict = {'cfgs' : [], 'name': name, 'displayed_name' : displayed_name, 'type' : '', 'description' : '', 'images' : []} 203 for index, item in enumerate(self._data): 204 if item['displayed_name'].lower() > displayed_name.lower(): 205 self._data.insert(index, new_dict) 206 return index 207 self._data.append(new_dict) 208 return len(self._data)-1
209
210 - def removeCfg(self, cfg):
211 ''' 212 Removes the configuration entries from objects and returns the list with 213 indexes, where the configuration was removed. 214 @param cfg: configuration to remove 215 @type cfg: C{str} 216 @return: the list the indexes, where the configuration was removed 217 @rtype: C{[int]} 218 ''' 219 result = [] 220 for index, d in enumerate(self._data): 221 if cfg in d['cfgs']: 222 d['cfgs'].remove(cfg) 223 result.append(index) 224 return result
225
226 - def count(self):
227 ''' 228 @return: The count of items in the header. 229 @rtype: C{int} 230 ''' 231 return len(self._data)
232
233 - def getConfigs(self, index):
234 ''' 235 @return: The configurations assigned to the item at the given index 236 @rtype: C{str} 237 ''' 238 result = [] 239 if index < len(self._data): 240 result = list(self._data[index]['cfgs']) 241 return result
242 243 244 ################################################################################ 245 ############## CapabilityControlWidget ############## 246 ################################################################################ 247
248 -class CapabilityControlWidget(QtGui.QFrame):
249 ''' 250 The control widget contains buttons for control a capability. Currently this 251 are C{On} and C{Off} buttons. Additionally, the state of the capability is 252 color coded. 253 ''' 254 255 start_nodes_signal = QtCore.Signal(str, str, list) 256 '''@ivar: the signal is emitted to start on host(described by masteruri) the nodes described in the list, Parameter(masteruri, config, nodes).''' 257 258 stop_nodes_signal = QtCore.Signal(str, list) 259 '''@ivar: the signal is emitted to stop on masteruri the nodes described in the list.''' 260
261 - def __init__(self, masteruri, cfg, ns, nodes, parent=None):
262 QtGui.QFrame.__init__(self, parent) 263 self._masteruri = masteruri 264 self._nodes = {cfg : {ns : nodes} } 265 frame_layout = QtGui.QVBoxLayout(self) 266 frame_layout.setContentsMargins(0, 0, 0, 0) 267 # create frame for warning label 268 self.warning_frame = warning_frame = QtGui.QFrame() 269 warning_layout = QtGui.QHBoxLayout(warning_frame) 270 warning_layout.setContentsMargins(0, 0, 0, 0) 271 warning_layout.addItem(QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) 272 self.warning_label = QtGui.QLabel() 273 icon = QtGui.QIcon(':/icons/crystal_clear_warning.png') 274 self.warning_label.setPixmap(icon.pixmap(QtCore.QSize(40, 40))) 275 self.warning_label.setToolTip('Multiple configuration for same node found!\nA first one will be selected for the start a node!') 276 warning_layout.addWidget(self.warning_label) 277 warning_layout.addItem(QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) 278 frame_layout.addItem(QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) 279 frame_layout.addWidget(warning_frame) 280 # create frame for start/stop buttons 281 buttons_frame = QtGui.QFrame() 282 buttons_layout = QtGui.QHBoxLayout(buttons_frame) 283 buttons_layout.setContentsMargins(0, 0, 0, 0) 284 buttons_layout.addItem(QtGui.QSpacerItem(20, 20)) 285 self.on_button = QtGui.QPushButton() 286 self.on_button.setFlat(False) 287 self.on_button.setText("On") 288 self.on_button.clicked.connect(self.on_on_clicked) 289 buttons_layout.addWidget(self.on_button) 290 291 self.off_button = QtGui.QPushButton() 292 self.off_button.setFlat(True) 293 self.off_button.setText("Off") 294 self.off_button.clicked.connect(self.on_off_clicked) 295 buttons_layout.addWidget(self.off_button) 296 buttons_layout.addItem(QtGui.QSpacerItem(20, 20)) 297 frame_layout.addWidget(buttons_frame) 298 frame_layout.addItem(QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) 299 self.warning_frame.setVisible(False)
300
301 - def hasConfigs(self):
302 ''' 303 @return: True, if a configurations for this widget are available. 304 @rtype: bool 305 ''' 306 return len(self._nodes) > 0
307
308 - def nodes(self, cfg=''):
309 ''' 310 @return: the list with nodes required by this capability. The nodes are 311 defined by ROS full name. 312 @rtype: C{[str]} 313 ''' 314 try: 315 if cfg: 316 return [n for l in self._nodes[cfg].itervalues() for n in l] 317 else: 318 return [n for c in self._nodes.itervalues() for l in c.itervalues() for n in l] 319 except: 320 return []
321
322 - def setNodeState(self, running_nodes, stopped_nodes, error_nodes):
323 ''' 324 Sets the state of this capability. 325 @param running_nodes: a list with running nodes. 326 @type running_nodes: C{[str]} 327 @param stopped_nodes: a list with not running nodes. 328 @type stopped_nodes: C{[str]} 329 @param error_nodes: a list with nodes having a problem. 330 @type error_nodes: C{[str]} 331 ''' 332 self.setAutoFillBackground(True) 333 self.setBackgroundRole(QtGui.QPalette.Base) 334 palette = QtGui.QPalette() 335 if error_nodes: 336 brush = QtGui.QBrush(QtGui.QColor(255, 100, 0)) 337 elif running_nodes and stopped_nodes: 338 brush = QtGui.QBrush(QtGui.QColor(140, 185, 255)) #30, 50, 255 339 elif running_nodes: 340 self.on_button.setFlat(True) 341 self.off_button.setFlat(False) 342 brush = QtGui.QBrush(QtGui.QColor(59, 223, 18)) # 59, 223, 18 343 else: 344 brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) 345 self.on_button.setFlat(False) 346 self.off_button.setFlat(True) 347 palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) 348 brush.setStyle(QtCore.Qt.SolidPattern) 349 palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) 350 self.setPalette(palette)
351
352 - def removeCfg(self, cfg):
353 try: 354 del self._nodes[cfg] 355 except: 356 pass
357
358 - def updateNodes(self, cfg, ns, nodes):
359 self._nodes[cfg] = {ns : nodes} 360 test_nodes = self.nodes() 361 self.warning_frame.setVisible(len(test_nodes) != len(set(test_nodes)))
362
363 - def on_on_clicked(self):
364 started = set() # do not start nodes multiple times 365 for c in self._nodes.iterkeys(): 366 node2start = set(self.nodes(c)) - started 367 self.start_nodes_signal.emit(self._masteruri, c, list(node2start)) 368 started.update(node2start) 369 self.on_button.setFlat(True) 370 self.off_button.setFlat(False)
371
372 - def on_off_clicked(self):
373 self.stop_nodes_signal.emit(self._masteruri, self.nodes()) 374 self.on_button.setFlat(False) 375 self.off_button.setFlat(True)
376 377 378 379 ################################################################################ 380 ############## CapabilityTable ############## 381 ################################################################################ 382
383 -class CapabilityTable(QtGui.QTableWidget):
384 ''' 385 The table shows all detected capabilities of robots in tabular view. The 386 columns represents the robot and rows the capabilities. The cell of available 387 capability contains a L{CapabilityControlWidget} to show the state and manage 388 the capability. 389 ''' 390 391 start_nodes_signal = QtCore.Signal(str, str, list) 392 '''@ivar: the signal is emitted to start on host(described by masteruri) the nodes described in the list, Parameter(masteruri, config, nodes).''' 393 394 stop_nodes_signal = QtCore.Signal(str, list) 395 '''@ivar: the signal is emitted to stop on masteruri the nodes described in the list.''' 396 397 description_requested_signal = QtCore.Signal(str, str) 398 '''@ivar: the signal is emitted by click on a header to show a description.''' 399 400
401 - def __init__(self, parent=None):
402 QtGui.QTableWidget.__init__(self, parent) 403 self._robotHeader = CapabilityHeader(QtCore.Qt.Horizontal, self) 404 self._robotHeader.description_requested_signal.connect(self._show_description) 405 self.setHorizontalHeader(self._robotHeader) 406 self._capabilityHeader = CapabilityHeader(QtCore.Qt.Vertical, self) 407 self._capabilityHeader.description_requested_signal.connect(self._show_description) 408 self.setVerticalHeader(self._capabilityHeader)
409
410 - def updateCapabilities(self, masteruri, cfg_name, description):
411 ''' 412 Updates the capabilities view. 413 @param masteruri: the ROS master URI of updated ROS master. 414 @type masteruri: C{str} 415 @param cfg_name: The name of the node provided the capabilities description. 416 @type cfg_name: C{str} 417 @param description: The capabilities description object. 418 @type description: L{default_cfg_fkie.Description} 419 ''' 420 # if it is a new masteruri add a new column 421 robot_index = self._robotHeader.index(masteruri) 422 robot_name = description.robot_name if description.robot_name else nm.nameres().mastername(masteruri) 423 if robot_index == -1: 424 # append a new robot 425 robot_index = self._robotHeader.insertSortedItem(masteruri, robot_name) 426 self.insertColumn(robot_index) 427 # robot_index = self.columnCount()-1 428 # self._robotHeader.insertItem(robot_index) 429 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) 430 item = QtGui.QTableWidgetItem() 431 item.setSizeHint(QtCore.QSize(96,96)) 432 self.setHorizontalHeaderItem(robot_index, item) 433 self.horizontalHeaderItem(robot_index).setText(robot_name) 434 else: 435 #update 436 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) 437 438 #set the capabilities 439 for c in description.capabilities: 440 cap_index = self._capabilityHeader.index(c.name.decode(sys.getfilesystemencoding())) 441 if cap_index == -1: 442 # append a new capability 443 cap_index = self._capabilityHeader.insertSortedItem(c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding())) 444 self.insertRow(cap_index) 445 self.setRowHeight(cap_index, 96) 446 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) 447 item = QtGui.QTableWidgetItem() 448 item.setSizeHint(QtCore.QSize(96,96)) 449 self.setVerticalHeaderItem(cap_index, item) 450 self.verticalHeaderItem(cap_index).setText(c.name.decode(sys.getfilesystemencoding())) 451 # add the capability control widget 452 controlWidget = CapabilityControlWidget(masteruri, cfg_name, c.namespace, c.nodes) 453 controlWidget.start_nodes_signal.connect(self._start_nodes) 454 controlWidget.stop_nodes_signal.connect(self._stop_nodes) 455 self.setCellWidget(cap_index, robot_index, controlWidget) 456 self._capabilityHeader.controlWidget.insert(cap_index, controlWidget) 457 else: 458 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) 459 self._capabilityHeader.controlWidget[cap_index].updateNodes(cfg_name, c.namespace, c.nodes)
460
461 - def removeConfig(self, cfg):
462 ''' 463 @param cfg: The name of the node provided the capabilities description. 464 @type cfg: C{str} 465 ''' 466 removed_from_robots = self._robotHeader.removeCfg(cfg) 467 # for r in removed_from_robots: 468 # if not self._robotHeader.getConfigs(r): 469 # #remove the column with robot 470 # pass 471 removed_from_caps = self._capabilityHeader.removeCfg(cfg) 472 # remove control widget with given configuration 473 for r in reversed(removed_from_robots): 474 for c in removed_from_caps: 475 controlWidget = self.cellWidget(c, r) 476 if isinstance(controlWidget, CapabilityControlWidget): 477 controlWidget.removeCfg(cfg) 478 if not controlWidget.hasConfigs(): 479 self.removeCellWidget(c, r) 480 # remove empty columns 481 for r in removed_from_robots: 482 is_empty = True 483 for c in reversed(range(self.rowCount())): 484 controlWidget = self.cellWidget(c, r) 485 if isinstance(controlWidget, CapabilityControlWidget): 486 is_empty = False 487 break 488 if is_empty: 489 self.removeColumn(r) 490 self._robotHeader.removeDescription(r) 491 # remove empty rows 492 for c in reversed(removed_from_caps): 493 is_empty = True 494 for r in reversed(range(self.columnCount())): 495 controlWidget = self.cellWidget(c, r) 496 if isinstance(controlWidget, CapabilityControlWidget): 497 is_empty = False 498 break 499 if is_empty: 500 self.removeRow(c) 501 self._capabilityHeader.removeDescription(c)
502
503 - def updateState(self, masteruri, master_info):
504 ''' 505 Updates the run state of the capability. 506 @param masteruri: The ROS master, which sends the master_info 507 @type masteruri: C{str} 508 @param master_info: The state of the ROS master 509 @type master_info: L{master_discovery_fkie.MasterInfo} 510 ''' 511 if master_info is None or masteruri is None: 512 return 513 robot_index = self._robotHeader.index(masteruri) 514 if robot_index != -1: 515 for c in range(self.rowCount()): 516 controlWidget = self.cellWidget(c, robot_index) 517 if not controlWidget is None: 518 running_nodes = [] 519 stopped_nodes = [] 520 error_nodes = [] 521 for n in controlWidget.nodes(): 522 node = master_info.getNode(n) 523 if not node is None: 524 # while a synchronization there are node from other hosts in the master_info -> filter these nodes 525 if not node.uri is None and masteruri == node.masteruri: 526 if not node.pid is None: 527 running_nodes.append(n) 528 else: 529 error_nodes.append(n) 530 else: 531 stopped_nodes.append(n) 532 controlWidget.setNodeState(running_nodes, stopped_nodes, error_nodes)
533
534 - def _start_nodes(self, masteruri, cfg, nodes):
535 self.start_nodes_signal.emit(masteruri, cfg, nodes)
536
537 - def _stop_nodes(self, masteruri, nodes):
539
540 - def _show_description(self, name, description):
541 self.description_requested_signal.emit(name, description)
542