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

Source Code for Module node_manager_fkie.node_tree_model

   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  from python_qt_binding.QtCore import QFile, Qt, Signal 
  34  from python_qt_binding.QtGui import QIcon, QStandardItem, QStandardItemModel 
  35  import re 
  36  import roslib 
  37  import rospy 
  38  import traceback 
  39   
  40  from master_discovery_fkie.common import get_hostname, subdomain 
  41  from master_discovery_fkie.master_info import NodeInfo 
  42  from node_manager_fkie.name_resolution import NameResolution 
  43  from parameter_handler import ParameterHandler 
  44  import node_manager_fkie as nm 
45 46 47 ################################################################################ 48 ############## GrouptItem ############## 49 ################################################################################ 50 -class GroupItem(QStandardItem):
51 ''' 52 The GroupItem stores the information about a group of nodes. 53 ''' 54 ITEM_TYPE = Qt.UserRole + 25 55
56 - def __init__(self, name, parent=None, has_remote_launched_nodes=False):
57 ''' 58 Initialize the GroupItem object with given values. 59 @param name: the name of the group 60 @type name: C{str} 61 @param parent: the parent item. In most cases this is the HostItem. The 62 variable is used to determine the different columns of the NodeItem. 63 @type parent: U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>} 64 ''' 65 QStandardItem.__init__(self, name if name.rfind('@') > 0 else '{' + name + '}') 66 self.parent_item = parent 67 self._name = name 68 self.setIcon(QIcon(':/icons/state_off.png')) 69 self.descr_type = self.descr_name = self.descr = '' 70 self.descr_images = [] 71 self._capcabilities = dict() 72 self._has_remote_launched_nodes = has_remote_launched_nodes 73 self._remote_launched_nodes_updated = False 74 ''' 75 @ivar: dict(config : dict(namespace: dict(group:dict('type' : str, 'images' : [str], 'description' : str, 'nodes' : [str])))) 76 ''' 77 self._re_cap_nodes = dict()
78 79 @property
80 - def name(self):
81 ''' 82 The name of this group. 83 @rtype: C{str} 84 ''' 85 return self._name
86 87 @name.setter
88 - def name(self, new_name):
89 ''' 90 Set the new name of this group and updates the displayed name of the item. 91 @param new_name: The new name of the group. Used also to identify the group. 92 @type new_name: C{str} 93 ''' 94 self._name = new_name 95 self.setText('{' + self._name + '}')
96
97 - def is_in_cap_group(self, nodename, config, ns, groupname):
98 ''' 99 Returns `True` if the group contains the node. 100 @param nodename: the name of the node to test 101 @type nodename: str 102 @param config: the configuration name 103 @type config: str 104 @param ns: namespace 105 @type ns: str 106 @param groupname: the group name 107 @type groupname: str 108 @return: `True`, if the nodename is in the group 109 @rtype: bool 110 ''' 111 try: 112 if self._re_cap_nodes[(config, ns, groupname)].match(nodename): 113 return True 114 except: 115 pass 116 return False
117
118 - def _create_cap_nodes_pattern(self, config, cap):
119 for ns, groups in cap.items(): 120 for groupname, descr in groups.items(): 121 try: 122 nodes = descr['nodes'] 123 def_list = ['\A' + n.strip().replace('*', '.*') + '\Z' for n in nodes] 124 if def_list: 125 self._re_cap_nodes[(config, ns, groupname)] = re.compile('|'.join(def_list), re.I) 126 else: 127 self._re_cap_nodes[(config, ns, groupname)] = re.compile('\b', re.I) 128 except: 129 rospy.logwarn("create_cap_nodes_pattern: %s" % traceback.format_exc(1))
130
131 - def addCapabilities(self, config, capabilities, masteruri):
132 ''' 133 Add new capabilities. Based on this capabilities the node are grouped. The 134 view will be updated. 135 @param config: The name of the configuration containing this new capabilities. 136 @type config: C{str} 137 @param masteruri: The masteruri is used only used, if new nodes are created. 138 @type masteruri: C{str} 139 @param capabilities: The capabilities, which defines groups and containing nodes. 140 @type capabilities: C{dict(namespace: dict(group:dict('type' : str, 'images' : [str], 'description' : str, 'nodes' : [str])))} 141 ''' 142 self._capcabilities[config] = capabilities 143 self._create_cap_nodes_pattern(config, capabilities) 144 # update the view 145 for ns, groups in capabilities.items(): 146 for group, descr in groups.items(): 147 group_changed = False 148 # create nodes for each group 149 nodes = descr['nodes'] 150 if nodes: 151 groupItem = self.getGroupItem(roslib.names.ns_join(ns, group)) 152 groupItem.descr_name = group 153 if descr['type']: 154 groupItem.descr_type = descr['type'] 155 if descr['description']: 156 groupItem.descr = descr['description'] 157 if descr['images']: 158 groupItem.descr_images = list(descr['images']) 159 # move the nodes from host to the group 160 for i in reversed(range(self.rowCount())): 161 item = self.child(i) 162 if isinstance(item, NodeItem) and self.is_in_cap_group(item.name, config, ns, group): 163 row = self.takeRow(i) 164 groupItem._addRow_sorted(row) 165 group_changed = True 166 167 # create new or update existing items in the group 168 for node_name in nodes: 169 # do not add nodes with * in the name 170 if not re.search(r"\*", node_name): 171 items = groupItem.getNodeItemsByName(node_name) 172 if items: 173 for item in items: 174 item.addConfig(config) 175 group_changed = True 176 else: 177 items = self.getNodeItemsByName(node_name) 178 if items: 179 # copy the state of the existing node 180 groupItem.addNode(items[0].node_info, config) 181 elif config: 182 groupItem.addNode(NodeInfo(node_name, masteruri), config) 183 group_changed = True 184 if group_changed: 185 groupItem.updateDisplayedConfig() 186 groupItem.updateIcon()
187
188 - def remCapablities(self, config):
189 ''' 190 Removes internal entry of the capability, so the new nodes are not grouped. 191 To update view L{NodeTreeModel.removeConfigNodes()} and L{GroupItem.clearUp()} 192 must be called. 193 @param config: The name of the configuration containing this new capabilities. 194 @type config: C{str} 195 ''' 196 try: 197 del self._capcabilities[config] 198 except: 199 pass 200 else: 201 # todo update view? 202 pass
203
204 - def getCapabilityGroups(self, node_name):
205 ''' 206 Returns the names of groups, which contains the given node. 207 @param node_name: The name of the node 208 @type node_name: C{str} 209 @return: The name of the configuration containing this new capabilities. 210 @rtype: C{dict(config : [str])} 211 ''' 212 result = dict() # dict(config : [group names]) 213 try: 214 for cfg, cap in self._capcabilities.items(): 215 for ns, groups in cap.items(): 216 for group, _ in groups.items(): # _:=decription 217 if self.is_in_cap_group(node_name, cfg, ns, group): 218 if cfg not in result: 219 result[cfg] = [] 220 result[cfg].append(roslib.names.ns_join(ns, group)) 221 except: 222 pass 223 # import traceback 224 # print traceback.format_exc(1) 225 return result
226
227 - def getNodeItemsByName(self, node_name, recursive=True):
228 ''' 229 Since the same node can be included by different groups, this method searches 230 for all nodes with given name and returns these items. 231 @param node_name: The name of the node 232 @type node_name: C{str} 233 @param recursive: Searches in (sub) groups 234 @type recursive: C{bool} 235 @return: The list with node items. 236 @rtype: C{[U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}]} 237 ''' 238 result = [] 239 for i in range(self.rowCount()): 240 item = self.child(i) 241 if isinstance(item, GroupItem): 242 if recursive: 243 result[len(result):] = item.getNodeItemsByName(node_name) 244 elif isinstance(item, NodeItem) and item == node_name: 245 return [item] 246 return result
247
248 - def getNodeItems(self, recursive=True):
249 ''' 250 Returns all nodes in this group and subgroups. 251 @param recursive: returns the nodes of the subgroups 252 @type recursive: bool 253 @return: The list with node items. 254 @rtype: C{[U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}]} 255 ''' 256 result = [] 257 for i in range(self.rowCount()): 258 item = self.child(i) 259 if isinstance(item, GroupItem): 260 if recursive: 261 result[len(result):] = item.getNodeItems() 262 elif isinstance(item, NodeItem): 263 result.append(item) 264 return result
265
266 - def getGroupItems(self):
267 ''' 268 Returns all group items this group 269 @return: The list with group items. 270 @rtype: C{[L{GroupItem}]} 271 ''' 272 result = [] 273 for i in range(self.rowCount()): 274 item = self.child(i) 275 if isinstance(item, GroupItem): 276 result.append(item) 277 result[len(result):] = item.getGroupItems() 278 return result
279
280 - def getGroupItem(self, group_name):
281 ''' 282 Returns a GroupItem with given name. If no group with this name exists, a 283 new one will be created. 284 Assumption: No groups in group!! 285 @param group_name: the name of the group 286 @type group_name: C{str} 287 @return: The group with given name 288 @rtype: L{GroupItem} 289 ''' 290 for i in range(self.rowCount()): 291 item = self.child(i) 292 if isinstance(item, GroupItem): 293 if item == group_name: 294 return item 295 elif item > group_name: 296 items = [] 297 newItem = GroupItem(group_name, self) 298 items.append(newItem) 299 cfgitem = QStandardItem() 300 items.append(cfgitem) 301 self.insertRow(i, items) 302 return newItem 303 items = [] 304 newItem = GroupItem(group_name, self) 305 items.append(newItem) 306 cfgitem = QStandardItem() 307 items.append(cfgitem) 308 self.appendRow(items) 309 return newItem
310
311 - def addNode(self, node, cfg=''):
312 ''' 313 Adds a new node with given name. 314 @param node: the NodeInfo of the node to create 315 @type node: L{NodeInfo} 316 @param cfg: The configuration, which describes the node 317 @type cfg: C{str} 318 ''' 319 groups = self.getCapabilityGroups(node.name) 320 if groups: 321 for _, group_list in groups.items(): 322 for group_name in group_list: 323 # insert in the group 324 groupItem = self.getGroupItem(group_name) 325 groupItem.addNode(node, cfg) 326 else: 327 # insert in order 328 new_item_row = NodeItem.newNodeRow(node.name, node.masteruri) 329 self._addRow_sorted(new_item_row) 330 new_item_row[0].node_info = node 331 if cfg or cfg == '': 332 new_item_row[0].addConfig(cfg)
333
334 - def _addRow_sorted(self, row):
335 for i in range(self.rowCount()): 336 item = self.child(i) 337 if item > row[0].name: 338 self.insertRow(i, row) 339 row[0].parent_item = self 340 return 341 self.appendRow(row) 342 row[0].parent_item = self
343
344 - def clearUp(self, fixed_node_names=None):
345 ''' 346 Removes not running and not configured nodes. 347 @param fixed_node_names: If the list is not None, the node not in the list are 348 set to not running! 349 @type fixed_node_names: C{[str]} 350 ''' 351 # first clear sub groups 352 groups = self.getGroupItems() 353 for group in groups: 354 group.clearUp(fixed_node_names) 355 removed = False 356 # move running nodes without configuration to the upper layer, remove not running and duplicate nodes 357 for i in reversed(range(self.rowCount())): 358 item = self.child(i) 359 if isinstance(item, NodeItem): 360 # set the running state of the node to None 361 if fixed_node_names is not None and item.name not in fixed_node_names: 362 item.node_info = NodeInfo(item.name, item.node_info.masteruri) 363 if not (item.has_configs() or item.is_running() or item.published or item.subscribed or item.services): 364 removed = True 365 self.removeRow(i) 366 elif not isinstance(self, HostItem): 367 has_launches = NodeItem.has_launch_cfgs(item.cfgs) 368 has_defaults = NodeItem.has_default_cfgs(item.cfgs) 369 has_std_cfg = item.has_std_cfg() 370 if item.state == NodeItem.STATE_RUN and not (has_launches or has_defaults or has_std_cfg): 371 # if it is in a group, is running, but has no configuration, move it to the host 372 if self.parent_item is not None and isinstance(self.parent_item, HostItem): 373 items_in_host = self.parent_item.getNodeItemsByName(item.name, True) 374 if len(items_in_host) == 1: 375 row = self.takeRow(i) 376 self.parent_item._addRow_sorted(row) 377 else: 378 # remove item 379 removed = True 380 self.removeRow(i) 381 if removed: 382 self.updateIcon() 383 384 # remove empty groups 385 for i in reversed(range(self.rowCount())): 386 item = self.child(i) 387 if isinstance(item, GroupItem): 388 if item.rowCount() == 0: 389 self.removeRow(i)
390
392 self._remote_launched_nodes_updated = False
393
395 if self._has_remote_launched_nodes: 396 return self._remote_launched_nodes_updated 397 return True
398
399 - def updateRunningNodeState(self, nodes):
400 ''' 401 Updates the running state of the nodes given in a dictionary. 402 @param nodes: A dictionary with node names and their running state described by L{NodeInfo}. 403 @type nodes: C{dict(str: U{master_discovery_fkie.NodeInfo<http://docs.ros.org/kinetic/api/master_discovery_fkie/html/modules.html#master_discovery_fkie.master_info.NodeInfo>})} 404 ''' 405 for (name, node) in nodes.items(): 406 # get the node items 407 items = self.getNodeItemsByName(name) 408 if items: 409 for item in items: 410 # update the node item 411 item.node_info = node 412 else: 413 # create the new node 414 self.addNode(node) 415 if self._has_remote_launched_nodes: 416 self._remote_launched_nodes_updated = True 417 self.clearUp(nodes.keys())
418
419 - def getRunningNodes(self):
420 ''' 421 Returns the names of all running nodes. A running node is defined by his 422 PID. 423 @see: U{master_dicovery_fkie.NodeInfo<http://docs.ros.org/kinetic/api/master_discovery_fkie/html/modules.html#master_discovery_fkie.master_info.NodeInfo>} 424 @return: A list with node names 425 @rtype: C{[str]} 426 ''' 427 result = [] 428 for i in range(self.rowCount()): 429 item = self.child(i) 430 if isinstance(item, GroupItem): 431 result[len(result):] = item.getRunningNodes() 432 elif isinstance(item, NodeItem) and item.node_info.pid is not None: 433 result.append(item.name) 434 return result
435
436 - def markNodesAsDuplicateOf(self, running_nodes, is_sync_running=False):
437 ''' 438 While a synchronization same node on different hosts have the same name, the 439 nodes with the same on other host are marked. 440 @param running_nodes: The dictionary with names of running nodes and their masteruri 441 @type running_nodes: C{dict(str:str)} 442 @param is_sync_running: If the master_sync is running, the nodes are marked 443 as ghost nodes. So they are handled as running nodes, but has not run 444 informations. This nodes are running on remote host, but are not 445 syncronized because of filter or errors. 446 @type is_sync_running: bool 447 ''' 448 ignore = ['/master_sync', '/master_discovery', '/node_manager'] 449 for i in range(self.rowCount()): 450 item = self.child(i) 451 if isinstance(item, GroupItem): 452 item.markNodesAsDuplicateOf(running_nodes, is_sync_running) 453 elif isinstance(item, NodeItem): 454 if is_sync_running: 455 item.is_ghost = (item.node_info.uri is None and (item.name in running_nodes and running_nodes[item.name] == item.node_info.masteruri)) 456 item.has_running = (item.node_info.uri is None and item.name not in ignore and (item.name in running_nodes and running_nodes[item.name] != item.node_info.masteruri)) 457 else: 458 if item.is_ghost: 459 item.is_ghost = False 460 item.has_running = (item.node_info.uri is None and item.name not in ignore and (item.name in running_nodes))
461
462 - def updateIcon(self):
463 if isinstance(self, HostItem): 464 # skip the icon update on a host item 465 return 466 has_running = False 467 has_off = False 468 has_duplicate = False 469 has_ghosts = False 470 diag_level = 0 471 for i in range(self.rowCount()): 472 item = self.child(i) 473 if isinstance(item, NodeItem): 474 if item.state == NodeItem.STATE_WARNING: 475 self.setIcon(QIcon(':/icons/crystal_clear_warning.png')) 476 return 477 elif item.state == NodeItem.STATE_OFF: 478 has_off = True 479 elif item.state == NodeItem.STATE_RUN: 480 has_running = True 481 if item.diagnostic_array and item.diagnostic_array[-1].level > 0: 482 if diag_level == 0: 483 diag_level = item.diagnostic_array[-1].level 484 elif item.diagnostic_array[-1].level == 2: 485 diag_level = 2 486 elif item.state == NodeItem.STATE_GHOST: 487 has_ghosts = True 488 elif item.state == NodeItem.STATE_DUPLICATE: 489 has_duplicate = True 490 diag_icon = None 491 if diag_level > 0: 492 diag_icon = NodeItem._diagnostic_level2icon(diag_level) 493 if has_duplicate: 494 self.setIcon(QIcon(':/icons/imacadam_stop.png')) 495 elif has_ghosts: 496 self.setIcon(QIcon(':/icons/state_ghost.png')) 497 elif has_running and has_off: 498 if diag_icon is not None: 499 self.setIcon(diag_icon) 500 else: 501 self.setIcon(QIcon(':/icons/state_part.png')) 502 elif not has_running: 503 self.setIcon(QIcon(':/icons/state_off.png')) 504 elif not has_off and has_running: 505 if diag_icon is not None: 506 self.setIcon(diag_icon) 507 else: 508 self.setIcon(QIcon(':/icons/state_run.png'))
509
510 - def _create_html_list(self, title, items):
511 result = '' 512 if items: 513 result += '<b><u>%s</u></b>' % title 514 if len(items) > 1: 515 result += ' <span style="color:gray;">[%d]</span>' % len(items) 516 result += '<ul><span></span><br>' 517 for i in items: 518 result += '<a href="node://%s">%s</a><br>' % (i, i) 519 result += '</ul>' 520 return result
521
522 - def updateTooltip(self):
523 ''' 524 Creates a tooltip description based on text set by L{updateDescription()} 525 and all childs of this host with valid sensor description. The result is 526 returned as a HTML part. 527 @return: the tooltip description coded as a HTML part 528 @rtype: C{str} 529 ''' 530 tooltip = self.generateDescription(False) 531 self.setToolTip(tooltip if tooltip else self.name) 532 return tooltip
533
534 - def generateDescription(self, extended=True):
535 tooltip = '' 536 if self.descr_type or self.descr_name or self.descr: 537 tooltip += '<h4>%s</h4><dl>' % self.descr_name 538 if self.descr_type: 539 tooltip += '<dt>Type: %s</dt></dl>' % self.descr_type 540 if extended: 541 try: 542 from docutils import examples 543 if self.descr: 544 tooltip += '<b><u>Detailed description:</u></b>' 545 tooltip += examples.html_body(unicode(self.descr)) 546 except: 547 rospy.logwarn("Error while generate description for a tooltip: %s", traceback.format_exc(1)) 548 tooltip += '<br>' 549 # get nodes 550 nodes = [] 551 for j in range(self.rowCount()): 552 nodes.append(self.child(j).name) 553 if nodes: 554 tooltip += self._create_html_list('Nodes:', nodes) 555 return '<div>%s</div>' % tooltip
556
557 - def updateDescription(self, descr_type, descr_name, descr):
558 ''' 559 Sets the description of the robot. To update the tooltip of the host item use L{updateTooltip()}. 560 @param descr_type: the type of the robot 561 @type descr_type: C{str} 562 @param descr_name: the name of the robot 563 @type descr_name: C{str} 564 @param descr: the description of the robot as a U{http://docutils.sourceforge.net/rst.html|reStructuredText} 565 @type descr: C{str} 566 ''' 567 self.descr_type = descr_type 568 self.descr_name = descr_name 569 self.descr = descr
570
571 - def updateDisplayedConfig(self):
572 ''' 573 Updates the configuration representation in other column. 574 ''' 575 if self.parent_item is not None: 576 # get nodes 577 cfgs = [] 578 for j in range(self.rowCount()): 579 if self.child(j).cfgs: 580 cfgs[len(cfgs):] = self.child(j).cfgs 581 if cfgs: 582 cfgs = list(set(cfgs)) 583 cfg_col = self.parent_item.child(self.row(), NodeItem.COL_CFG) 584 if cfg_col is not None and isinstance(cfg_col, QStandardItem): 585 cfg_col.setText('[%d]' % len(cfgs) if len(cfgs) > 1 else "") 586 # set tooltip 587 # removed for clarity !!! 588 # tooltip = '' 589 # if len(cfgs) > 0: 590 # tooltip = '' 591 # if len(cfgs) > 0: 592 # tooltip = ''.join([tooltip, '<h4>', 'Configurations:', '</h4><dl>']) 593 # for c in cfgs: 594 # if NodeItem.is_default_cfg(c): 595 # tooltip = ''.join([tooltip, '<dt>[default]', c[0], '</dt>']) 596 # else: 597 # tooltip = ''.join([tooltip, '<dt>', c, '</dt>']) 598 # tooltip = ''.join([tooltip, '</dl>']) 599 # cfg_col.setToolTip(''.join(['<div>', tooltip, '</div>'])) 600 # set icons 601 has_launches = NodeItem.has_launch_cfgs(cfgs) 602 has_defaults = NodeItem.has_default_cfgs(cfgs) 603 if has_launches and has_defaults: 604 cfg_col.setIcon(QIcon(':/icons/crystal_clear_launch_file_def_cfg.png')) 605 elif has_launches: 606 cfg_col.setIcon(QIcon(':/icons/crystal_clear_launch_file.png')) 607 elif has_defaults: 608 cfg_col.setIcon(QIcon(':/icons/default_cfg.png')) 609 else: 610 cfg_col.setIcon(QIcon())
611
612 - def type(self):
613 return GroupItem.ITEM_TYPE
614
615 - def __eq__(self, item):
616 ''' 617 Compares the name of the group. 618 ''' 619 if isinstance(item, str) or isinstance(item, unicode): 620 return self.name.lower() == item.lower() 621 elif not (item is None): 622 return self.name.lower() == item.name.lower() 623 return False
624
625 - def __gt__(self, item):
626 ''' 627 Compares the name of the group. 628 ''' 629 if isinstance(item, str) or isinstance(item, unicode): 630 return self.name.lower() > item.lower() 631 elif not (item is None): 632 return self.name.lower() > item.name.lower() 633 return False
634
635 636 ################################################################################ 637 ############## HostItem ############## 638 ################################################################################ 639 640 -class HostItem(GroupItem):
641 ''' 642 The HostItem stores the information about a host. 643 ''' 644 ITEM_TYPE = Qt.UserRole + 26 645
646 - def __init__(self, masteruri, address, local, parent=None):
647 ''' 648 Initialize the HostItem object with given values. 649 @param masteruri: URI of the ROS master assigned to the host 650 @type masteruri: C{str} 651 @param address: the address of the host 652 @type address: C{str} 653 @param local: is this host the localhost where the node_manager is running. 654 @type local: C{bool} 655 ''' 656 self._has_remote_launched_nodes = False 657 name = self.create_host_description(masteruri, address) 658 self._masteruri = masteruri 659 self._host = address 660 self._mastername = address 661 self._local = local 662 GroupItem.__init__(self, name, parent, has_remote_launched_nodes=self._has_remote_launched_nodes) 663 image_file = nm.settings().robot_image_file(name) 664 if QFile.exists(image_file): 665 self.setIcon(QIcon(image_file)) 666 else: 667 if local: 668 self.setIcon(QIcon(':/icons/crystal_clear_miscellaneous.png')) 669 else: 670 self.setIcon(QIcon(':/icons/remote.png')) 671 self.descr_type = self.descr_name = self.descr = ''
672 673 @property
674 - def host(self):
675 return self._host
676 677 @property
678 - def hostname(self):
679 return nm.nameres().hostname(self._host)
680 681 @property
682 - def address(self):
683 if NameResolution.is_legal_ip(self._host): 684 return self._host 685 else: 686 result = nm.nameres().resolve_cached(self._host) 687 if result: 688 return result[0] 689 return None
690 691 @property
692 - def addresses(self):
693 return nm.nameres().resolve_cached(self._host)
694 695 @property
696 - def masteruri(self):
697 return self._masteruri
698 699 @property
700 - def mastername(self):
701 result = nm.nameres().mastername(self._masteruri, self._host) 702 if not result: 703 result = self.hostname 704 return result
705
706 - def create_host_description(self, masteruri, address):
707 ''' 708 Returns the name generated from masteruri and address 709 @param masteruri: URI of the ROS master assigned to the host 710 @type masteruri: C{str} 711 @param address: the address of the host 712 @type address: C{str} 713 ''' 714 name = nm.nameres().mastername(masteruri, address) 715 if not name: 716 name = address 717 hostname = nm.nameres().hostname(address) 718 if hostname is None: 719 hostname = str(address) 720 if not nm.settings().show_domain_suffix: 721 name = subdomain(name) 722 result = '%s@%s' % (name, hostname) 723 if get_hostname(masteruri) != hostname: 724 result += '[%s]' % masteruri 725 self._has_remote_launched_nodes = True 726 return result
727
728 - def updateTooltip(self):
729 ''' 730 Creates a tooltip description based on text set by L{updateDescription()} 731 and all childs of this host with valid sensor description. The result is 732 returned as a HTML part. 733 @return: the tooltip description coded as a HTML part 734 @rtype: C{str} 735 ''' 736 tooltip = self.generateDescription(False) 737 self.setToolTip(tooltip if tooltip else self.name) 738 return tooltip
739
740 - def generateDescription(self, extended=True):
741 from docutils import examples 742 tooltip = '' 743 if self.descr_type or self.descr_name or self.descr: 744 tooltip += '<h4>%s</h4><dl>' % self.descr_name 745 if self.descr_type: 746 tooltip += '<dt>Type: %s</dt></dl>' % self.descr_type 747 if extended: 748 try: 749 if self.descr: 750 tooltip += '<b><u>Detailed description:</u></b>' 751 tooltip += examples.html_body(self.descr, input_encoding='utf8') 752 except: 753 rospy.logwarn("Error while generate description for a tooltip: %s", traceback.format_exc(1)) 754 tooltip += '<br>' 755 tooltip += '<h3>%s</h3>' % self.mastername 756 tooltip += '<font size="+1"><i>%s</i></font><br>' % self.masteruri 757 tooltip += '<font size="+1">Host: <b>%s%s</b></font><br>' % (self.hostname, ' %s' % self.addresses if self.addresses else '') 758 tooltip += '<a href="open-sync-dialog://%s">open sync dialog</a>' % (str(self.masteruri).replace('http://', '')) 759 tooltip += '<p>' 760 tooltip += '<a href="show-all-screens://%s">show all screens</a>' % (str(self.masteruri).replace('http://', '')) 761 tooltip += '<p>' 762 # if not nm.is_local(self.address): 763 tooltip += '<a href="poweroff://%s" title="calls `sudo poweroff` at `%s` via SSH">poweroff `%s`</a>' % (self.hostname, self.hostname, self.hostname) 764 tooltip += '<p>' 765 tooltip += '<a href="remove-all-launch-server://%s">kill all launch server</a>' % str(self.masteruri).replace('http://', '') 766 tooltip += '<p>' 767 # get sensors 768 capabilities = [] 769 for j in range(self.rowCount()): 770 item = self.child(j) 771 if isinstance(item, GroupItem): 772 capabilities.append(item.name) 773 if capabilities: 774 tooltip += '<b><u>Capabilities:</u></b>' 775 try: 776 tooltip += examples.html_body('- %s' % ('\n- '.join(capabilities)), input_encoding='utf8') 777 except: 778 rospy.logwarn("Error while generate description for a tooltip: %s", traceback.format_exc(1)) 779 return '<div>%s</div>' % tooltip if tooltip else ''
780
781 - def type(self):
782 return HostItem.ITEM_TYPE
783
784 - def __eq__(self, item):
785 ''' 786 Compares the address of the masteruri. 787 ''' 788 if isinstance(item, str) or isinstance(item, unicode): 789 rospy.logwarn("compare HostItem with unicode depricated") 790 return False 791 elif isinstance(item, tuple): 792 return self.masteruri == item[0] and self.host == item[1] 793 elif isinstance(item, HostItem): 794 return self.masteruri == item.masteruri and self.host == item.host 795 return False
796
797 - def __gt__(self, item):
798 ''' 799 Compares the address of the masteruri. 800 ''' 801 if isinstance(item, str) or isinstance(item, unicode): 802 rospy.logwarn("compare HostItem with unicode depricated") 803 return False 804 elif isinstance(item, tuple): 805 return self.masteruri > item[0] 806 elif isinstance(item, HostItem): 807 return self.masteruri > item.masteruri 808 return False
809
810 811 ################################################################################ 812 ############## NodeItem ############## 813 ################################################################################ 814 815 -class NodeItem(QStandardItem):
816 ''' 817 The NodeItem stores the information about the node using the ExtendedNodeInfo 818 class and represents it in a U{QTreeView<https://srinikom.github.io/pyside-docs/PySide/QtGui/QTreeView.html>} using the 819 U{QStandardItemModel<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItemModel.html>} 820 ''' 821 822 ITEM_TYPE = QStandardItem.UserType + 35 823 NAME_ROLE = Qt.UserRole + 1 824 COL_CFG = 1 825 # COL_URI = 2 826 827 STATE_OFF = 0 828 STATE_RUN = 1 829 STATE_WARNING = 2 830 STATE_GHOST = 3 831 STATE_DUPLICATE = 4 832
833 - def __init__(self, node_info):
834 ''' 835 Initialize the NodeItem instance. 836 @param node_info: the node information 837 @type node_info: U{master_discovery_fkie.NodeInfo<http://docs.ros.org/kinetic/api/master_discovery_fkie/html/modules.html#master_discovery_fkie.master_info.NodeInfo>} 838 ''' 839 QStandardItem.__init__(self, node_info.name) 840 self.parent_item = None 841 self._node_info = node_info.copy() 842 # self.ICONS = {'empty' : QIcon(), 843 # 'run' : QIcon(':/icons/state_run.png'), 844 # 'off' :QIcon(':/icons/state_off.png'), 845 # 'warning' : QIcon(':/icons/crystal_clear_warning.png'), 846 # 'stop' : QIcon('icons/imacadam_stop.png'), 847 # 'cfg+def' : QIcon(':/icons/crystal_clear_launch_file_def_cfg.png'), 848 # 'cfg' : QIcon(':/icons/crystal_clear_launch_file.png'), 849 # 'default_cfg' : QIcon(':/icons/default_cfg.png') 850 # } 851 self._cfgs = [] 852 self._std_config = None # it's config with empty name. for default proposals 853 self._is_ghost = False 854 self._has_running = False 855 self.setIcon(QIcon(':/icons/state_off.png')) 856 self._state = NodeItem.STATE_OFF 857 self.diagnostic_array = []
858 859 @property
860 - def state(self):
861 return self._state
862 863 @property
864 - def name(self):
865 return self._node_info.name
866 867 @property
868 - def masteruri(self):
869 return self._node_info.masteruri
870 871 @property
872 - def published(self):
873 return self._node_info.publishedTopics
874 875 @property
876 - def subscribed(self):
877 return self._node_info.subscribedTopics
878 879 @property
880 - def services(self):
881 return self._node_info.services
882 883 @property
884 - def node_info(self):
885 ''' 886 Returns the NodeInfo instance of this node. 887 @rtype: U{master_discovery_fkie.NodeInfo<http://docs.ros.org/kinetic/api/master_discovery_fkie/html/modules.html#master_discovery_fkie.master_info.NodeInfo>} 888 ''' 889 return self._node_info
890 891 @node_info.setter
892 - def node_info(self, node_info):
893 ''' 894 Sets the NodeInfo and updates the view, if needed. 895 ''' 896 abbos_changed = False 897 run_changed = False 898 # print "!!!", self.name 899 # print " subs: ", self._node_info.subscribedTopics, node_info.subscribedTopics 900 # print " pubs: ", self._node_info.publishedTopics, node_info.publishedTopics 901 # print " srvs: ", self._node_info.services, node_info.services 902 if self._node_info.publishedTopics != node_info.publishedTopics: 903 abbos_changed = True 904 self._node_info._publishedTopics = list(node_info.publishedTopics) 905 if self._node_info.subscribedTopics != node_info.subscribedTopics: 906 abbos_changed = True 907 self._node_info._subscribedTopics = list(node_info.subscribedTopics) 908 if self._node_info.services != node_info.services: 909 abbos_changed = True 910 self._node_info._services = list(node_info.services) 911 if self._node_info.pid != node_info.pid: 912 self._node_info.pid = node_info.pid 913 run_changed = True 914 if self._node_info.uri != node_info.uri: 915 self._node_info.uri = node_info.uri 916 run_changed = True 917 # update the tooltip and icon 918 if run_changed and (self.is_running() or self.has_configs) or abbos_changed: 919 self.updateDispayedName() 920 # self.updateDisplayedURI() 921 if self.parent_item is not None and not isinstance(self.parent_item, HostItem): 922 self.parent_item.updateIcon()
923 924 @property
925 - def uri(self):
926 return self._node_info.uri
927 928 @property
929 - def pid(self):
930 return self._node_info.pid
931 932 @property
933 - def has_running(self):
934 ''' 935 Returns C{True}, if there are exists other nodes with the same name. This 936 variable must be set manually! 937 @rtype: C{bool} 938 ''' 939 return self._has_running
940 941 @has_running.setter
942 - def has_running(self, state):
943 ''' 944 Sets however other node with the same name are running or not (on other hosts) 945 and updates the view of this item. 946 ''' 947 if self._has_running != state: 948 self._has_running = state 949 if self.has_configs() or self.is_running(): 950 self.updateDispayedName() 951 if self.parent_item is not None and not isinstance(self.parent_item, HostItem): 952 self.parent_item.updateIcon()
953 954 @property
955 - def is_ghost(self):
956 ''' 957 Returns C{True}, if there are exists other runnig nodes with the same name. This 958 variable must be set manually! 959 @rtype: C{bool} 960 ''' 961 return self._is_ghost
962 963 @is_ghost.setter
964 - def is_ghost(self, state):
965 ''' 966 Sets however other node with the same name is running (on other hosts) and 967 and the host showing this node the master_sync is running, but the node is 968 not synchronized. 969 ''' 970 if self._is_ghost != state: 971 self._is_ghost = state 972 if self.has_configs() or self.is_running(): 973 self.updateDispayedName() 974 if self.parent_item is not None and not isinstance(self.parent_item, HostItem): 975 self.parent_item.updateIcon()
976
977 - def append_diagnostic_status(self, diagnostic_status):
978 self.diagnostic_array.append(diagnostic_status) 979 self.updateDispayedName() 980 if self.parent_item is not None and not isinstance(self.parent_item, HostItem): 981 self.parent_item.updateIcon() 982 if len(self.diagnostic_array) > 15: 983 del self.diagnostic_array[0]
984
985 - def data(self, role):
986 if role == self.NAME_ROLE: 987 return self.name 988 else: 989 return QStandardItem.data(self, role)
990 991 @staticmethod
992 - def _diagnostic_level2icon(level):
993 if level == 1: 994 return QIcon(':/icons/state_diag_warn.png') 995 elif level == 2: 996 return QIcon(':/icons/state_diag_error.png') 997 elif level == 3: 998 return QIcon(':/icons/state_diag_stale.png') 999 else: 1000 return QIcon(':/icons/state_diag_other.png')
1001
1002 - def updateDispayedName(self):
1003 ''' 1004 Updates the name representation of the Item 1005 ''' 1006 tooltip = '<h4>%s</h4><dl>' % self.node_info.name 1007 tooltip += '<dt><b>URI:</b> %s</dt>' % self.node_info.uri 1008 tooltip += '<dt><b>PID:</b> %s</dt>' % self.node_info.pid 1009 tooltip += '<dt><b>ORG.MASTERURI:</b> %s</dt></dl>' % self.node_info.masteruri 1010 master_discovered = nm.nameres().has_master(self.node_info.masteruri) 1011 # local = False 1012 # if not self.node_info.uri is None and not self.node_info.masteruri is None: 1013 # local = (get_hostname(self.node_info.uri) == get_hostname(self.node_info.masteruri)) 1014 if self.node_info.pid is not None: 1015 self._state = NodeItem.STATE_RUN 1016 if self.diagnostic_array and self.diagnostic_array[-1].level > 0: 1017 level = self.diagnostic_array[-1].level 1018 self.setIcon(self._diagnostic_level2icon(level)) 1019 self.setToolTip(self.diagnostic_array[-1].message) 1020 else: 1021 self.setIcon(QIcon(':/icons/state_run.png')) 1022 self.setToolTip('') 1023 elif self.node_info.uri is not None and not self.node_info.isLocal: 1024 self._state = NodeItem.STATE_RUN 1025 self.setIcon(QIcon(':/icons/state_unknown.png')) 1026 tooltip += '<dl><dt>(Remote nodes will not be ping, so they are always marked running)</dt></dl>' 1027 tooltip += '</dl>' 1028 self.setToolTip('<div>%s</div>' % tooltip) 1029 # elif not self.node_info.isLocal and not master_discovered and not self.node_info.uri is None: 1030 # # elif not local and not master_discovered and not self.node_info.uri is None: 1031 # self._state = NodeItem.STATE_RUN 1032 # self.setIcon(QIcon(':/icons/state_run.png')) 1033 # tooltip = ''.join([tooltip, '<dl><dt>(Remote nodes will not be ping, so they are always marked running)</dt></dl>']) 1034 # tooltip = ''.join([tooltip, '</dl>']) 1035 # self.setToolTip(''.join(['<div>', tooltip, '</div>'])) 1036 elif self.node_info.pid is None and self.node_info.uri is None and (self.node_info.subscribedTopics or self.node_info.publishedTopics or self.node_info.services): 1037 self.setIcon(QIcon(':/icons/crystal_clear_warning.png')) 1038 self._state = NodeItem.STATE_WARNING 1039 tooltip += '<dl><dt>Can\'t get node contact information, but there exists publisher, subscriber or services of this node.</dt></dl>' 1040 tooltip += '</dl>' 1041 self.setToolTip('<div>%s</div>' % tooltip) 1042 elif self.node_info.uri is not None: 1043 self._state = NodeItem.STATE_WARNING 1044 self.setIcon(QIcon(':/icons/crystal_clear_warning.png')) 1045 if not self.node_info.isLocal and master_discovered: 1046 tooltip = '<h4>%s is not local, however the ROS master on this host is discovered, but no information about this node received!</h4>' % self.node_info.name 1047 self.setToolTip('<div>%s</div>' % tooltip) 1048 elif self.is_ghost: 1049 self._state = NodeItem.STATE_GHOST 1050 self.setIcon(QIcon(':/icons/state_ghost.png')) 1051 tooltip = '<h4>The node is running, but not synchronized because of filter or errors, see master_sync log.</h4>' 1052 self.setToolTip('<div>%s</div>' % tooltip) 1053 elif self.has_running: 1054 self._state = NodeItem.STATE_DUPLICATE 1055 self.setIcon(QIcon(':/icons/imacadam_stop.png')) 1056 tooltip = '<h4>There are nodes with the same name on remote hosts running. These will be terminated, if you run this node! (Only if master_sync is running or will be started somewhere!)</h4>' 1057 self.setToolTip('<div>%s</div>' % tooltip) 1058 else: 1059 self._state = NodeItem.STATE_OFF 1060 self.setIcon(QIcon(':/icons/state_off.png')) 1061 self.setToolTip('')
1062 # removed common tooltip for clarity !!! 1063 # self.setToolTip(''.join(['<div>', tooltip, '</div>'])) 1064
1065 - def updateDisplayedURI(self):
1066 ''' 1067 Updates the URI representation in other column. 1068 ''' 1069 if self.parent_item is not None: 1070 uri_col = self.parent_item.child(self.row(), NodeItem.COL_URI) 1071 if uri_col is not None and isinstance(uri_col, QStandardItem): 1072 uri_col.setText(str(self.node_info.uri) if self.node_info.uri is not None else "")
1073 1074 @property
1075 - def cfgs(self):
1076 ''' 1077 Returns the list with all launch configurations assigned to this item. 1078 @rtype: C{[str]} 1079 ''' 1080 return self._cfgs
1081
1082 - def addConfig(self, cfg):
1083 ''' 1084 Add the given configurations to the node. 1085 @param cfg: the loaded configuration, which contains this node. 1086 @type cfg: C{str} 1087 ''' 1088 if cfg == '': 1089 self._std_config = cfg 1090 elif cfg and cfg not in self._cfgs: 1091 self._cfgs.append(cfg) 1092 self.updateDisplayedConfig()
1093
1094 - def remConfig(self, cfg):
1095 ''' 1096 Remove the given configurations from the node. 1097 @param cfg: the loaded configuration, which contains this node. 1098 @type cfg: C{str} 1099 ''' 1100 result = False 1101 if cfg == '': 1102 self._std_config = None 1103 result = True 1104 if cfg in self._cfgs: 1105 self._cfgs.remove(cfg) 1106 result = True 1107 if result and (self.has_configs() or self.is_running()): 1108 self.updateDisplayedConfig() 1109 return result
1110
1111 - def updateDisplayedConfig(self):
1112 ''' 1113 Updates the configuration representation in other column. 1114 ''' 1115 if self.parent_item is not None: 1116 cfg_col = self.parent_item.child(self.row(), NodeItem.COL_CFG) 1117 if cfg_col is not None and isinstance(cfg_col, QStandardItem): 1118 cfg_count = len(self._cfgs) 1119 cfg_col.setText(str(''.join(['[', str(cfg_count), ']'])) if cfg_count > 1 else "") 1120 # set tooltip 1121 # removed tooltip for clarity !!! 1122 # tooltip = '' 1123 # if len(self._cfgs) > 0: 1124 # tooltip = '' 1125 # if len(self._cfgs) > 0: 1126 # tooltip = ''.join([tooltip, '<h4>', 'Configurations:', '</h4><dl>']) 1127 # for c in self._cfgs: 1128 # if NodeItem.is_default_cfg(c): 1129 # tooltip = ''.join([tooltip, '<dt>[default]', c[0], '</dt>']) 1130 # else: 1131 # tooltip = ''.join([tooltip, '<dt>', c, '</dt>']) 1132 # tooltip = ''.join([tooltip, '</dl>']) 1133 # cfg_col.setToolTip(''.join(['<div>', tooltip, '</div>'])) 1134 # set icons 1135 has_launches = NodeItem.has_launch_cfgs(self._cfgs) 1136 has_defaults = NodeItem.has_default_cfgs(self._cfgs) 1137 if has_launches and has_defaults: 1138 cfg_col.setIcon(QIcon(':/icons/crystal_clear_launch_file_def_cfg.png')) 1139 elif has_launches: 1140 cfg_col.setIcon(QIcon(':/icons/crystal_clear_launch_file.png')) 1141 elif has_defaults: 1142 cfg_col.setIcon(QIcon(':/icons/default_cfg.png')) 1143 else: 1144 cfg_col.setIcon(QIcon())
1145 # the update of the group will be perform in node_tree_model to reduce calls 1146 # if isinstance(self.parent_item, GroupItem): 1147 # self.parent_item.updateDisplayedConfig() 1148
1149 - def type(self):
1150 return NodeItem.ITEM_TYPE
1151 1152 @classmethod
1153 - def newNodeRow(self, name, masteruri):
1154 ''' 1155 Creates a new node row and returns it as a list with items. This list is 1156 used for the visualization of node data as a table row. 1157 @param name: the node name 1158 @type name: C{str} 1159 @param masteruri: the URI or the ROS master assigned to this node. 1160 @type masteruri: C{str} 1161 @return: the list for the representation as a row 1162 @rtype: C{[L{NodeItem}, U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}(Cofigurations), U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}(Node URI)]} 1163 ''' 1164 items = [] 1165 item = NodeItem(NodeInfo(name, masteruri)) 1166 items.append(item) 1167 cfgitem = QStandardItem() 1168 items.append(cfgitem) 1169 # uriitem = QStandardItem() 1170 # items.append(uriitem) 1171 return items
1172
1173 - def has_configs(self):
1174 return not (len(self._cfgs) == 0) # and self._std_config is None)
1175
1176 - def is_running(self):
1177 return not (self._node_info.pid is None and self._node_info.uri is None)
1178
1179 - def has_std_cfg(self):
1180 return self._std_config == ''
1181 1182 @classmethod
1183 - def has_launch_cfgs(cls, cfgs):
1184 for c in cfgs: 1185 if not cls.is_default_cfg(c): 1186 return True 1187 return False
1188 1189 @classmethod
1190 - def has_default_cfgs(cls, cfgs):
1191 for c in cfgs: 1192 if cls.is_default_cfg(c): 1193 return True 1194 return False
1195 1196 @classmethod
1197 - def is_default_cfg(cls, cfg):
1198 return isinstance(cfg, tuple)
1199
1200 - def __eq__(self, item):
1201 ''' 1202 Compares the name of the node. 1203 ''' 1204 if isinstance(item, str) or isinstance(item, unicode): 1205 return self.name == item 1206 elif not (item is None): 1207 return self.name == item.name 1208 return False
1209
1210 - def __gt__(self, item):
1211 ''' 1212 Compares the name of the node. 1213 ''' 1214 if isinstance(item, str) or isinstance(item, unicode): 1215 return self.name > item 1216 elif not (item is None): 1217 return self.name > item.name 1218 return False
1219
1220 1221 ################################################################################ 1222 ############## NodeTreeModel ############## 1223 ################################################################################ 1224 1225 -class NodeTreeModel(QStandardItemModel):
1226 ''' 1227 The model to show the nodes running in a ROS system or loaded by a launch 1228 configuration. 1229 ''' 1230 # ICONS = {'default' : QIcon(), 1231 # 'run' : QIcon(":/icons/state_run.png"), 1232 # 'warning' : QIcon(":/icons/crystal_clear_warning.png"), 1233 # 'def_launch_cfg' : QIcon(":/icons/crystal_clear_launch_file_def_cfg.png"), 1234 # 'launch_cfg' : QIcon(":/icons/crystal_clear_launch_file.png"), 1235 # 'def_cfg' : QIcon(":/icons/default_cfg.png") } 1236 1237 header = [('Name', 450), 1238 ('Cfgs', -1)] 1239 # ('URI', -1)] 1240 1241 hostInserted = Signal(HostItem) 1242 '''@ivar: the Qt signal, which is emitted, if a new host was inserted. 1243 Parameter: U{QtCore.QModelIndex<https://srinikom.github.io/pyside-docs/PySide/QtCore/QModelIndex.html>} of the inserted host item''' 1244
1245 - def __init__(self, host_address, masteruri, parent=None):
1246 ''' 1247 Initialize the model. 1248 ''' 1249 super(NodeTreeModel, self).__init__(parent) 1250 self.setColumnCount(len(NodeTreeModel.header)) 1251 self.setHorizontalHeaderLabels([label for label, _ in NodeTreeModel.header]) 1252 self._local_host_address = host_address 1253 self._local_masteruri = masteruri 1254 self._std_capabilities = {'': {'SYSTEM': {'images': [], 1255 'nodes': ['/rosout', 1256 '/master_discovery', 1257 '/zeroconf', 1258 '/master_sync', 1259 '/node_manager', 1260 '/dynamic_reconfigure/*'], 1261 'type': '', 1262 'description': 'This group contains the system management nodes.'}}} 1263 1264 # create a handler to request the parameter 1265 self.parameterHandler = ParameterHandler() 1266 # self.parameterHandler.parameter_list_signal.connect(self._on_param_list) 1267 self.parameterHandler.parameter_values_signal.connect(self._on_param_values)
1268 # self.parameterHandler.delivery_result_signal.connect(self._on_delivered_values) 1269 1270 @property
1271 - def local_addr(self):
1272 return self._local_host_address
1273
1274 - def flags(self, index):
1275 if not index.isValid(): 1276 return Qt.NoItemFlags 1277 return Qt.ItemIsEnabled | Qt.ItemIsSelectable
1278
1279 - def _set_std_capabilities(self, host_item):
1280 if host_item is not None: 1281 cap = self._std_capabilities 1282 mastername = roslib.names.SEP.join(['', host_item.mastername, '*', 'default_cfg']) 1283 if mastername not in cap['']['SYSTEM']['nodes']: 1284 cap['']['SYSTEM']['nodes'].append(mastername) 1285 host_item.addCapabilities('', cap, host_item.masteruri) 1286 return cap 1287 return dict(self._std_capabilities)
1288
1289 - def get_hostitem(self, masteruri, address):
1290 ''' 1291 Searches for the host item in the model. If no item is found a new one will 1292 created and inserted in sorted order. 1293 @param masteruri: ROS master URI 1294 @type masteruri: C{str} 1295 @param address: the address of the host 1296 @type address: C{str} 1297 @return: the item associated with the given master 1298 @rtype: L{HostItem} 1299 ''' 1300 if masteruri is None: 1301 return None 1302 host = (masteruri, address) 1303 # [address] + nm.nameres().resolve_cached(address) 1304 local = (self.local_addr in [address] + nm.nameres().resolve_cached(address) and 1305 self._local_masteruri == masteruri) 1306 # find the host item by address 1307 root = self.invisibleRootItem() 1308 for i in range(root.rowCount()): 1309 if root.child(i) == host: 1310 return root.child(i) 1311 elif root.child(i) > host: 1312 hostItem = HostItem(masteruri, address, local) 1313 self.insertRow(i, hostItem) 1314 self.hostInserted.emit(hostItem) 1315 self._set_std_capabilities(hostItem) 1316 return hostItem 1317 hostItem = HostItem(masteruri, address, local) 1318 self.appendRow(hostItem) 1319 self.hostInserted.emit(hostItem) 1320 self._set_std_capabilities(hostItem) 1321 return hostItem
1322
1323 - def updateModelData(self, nodes):
1324 ''' 1325 Updates the model data. 1326 @param nodes: a dictionary with name and info objects of the nodes. 1327 @type nodes: C{dict(str:L{NodeInfo}, ...)} 1328 ''' 1329 # separate into different hosts 1330 hosts = dict() 1331 addresses = [] 1332 for i in reversed(range(self.invisibleRootItem().rowCount())): 1333 host = self.invisibleRootItem().child(i) 1334 host.reset_remote_launched_nodes() 1335 for (name, node) in nodes.items(): 1336 addr = get_hostname(node.uri if node.uri is not None else node.masteruri) 1337 addresses.append(node.masteruri) 1338 host = (node.masteruri, addr) 1339 if host not in hosts: 1340 hosts[host] = dict() 1341 hosts[host][name] = node 1342 # update nodes for each host 1343 for ((masteruri, host), nodes_filtered) in hosts.items(): 1344 hostItem = self.get_hostitem(masteruri, host) 1345 # rename the host item if needed 1346 if hostItem is not None: 1347 hostItem.updateRunningNodeState(nodes_filtered) 1348 # request for all nodes in host the parameter capability_group 1349 self._requestCapabilityGroupParameter(hostItem) 1350 # update nodes of the hosts, which are not more exists 1351 for i in reversed(range(self.invisibleRootItem().rowCount())): 1352 host = self.invisibleRootItem().child(i) 1353 if host.masteruri not in addresses: 1354 host.updateRunningNodeState({}) 1355 self.removeEmptyHosts()
1356 # update the duplicate state 1357 # self.markNodesAsDuplicateOf(self.getRunningNodes()) 1358
1359 - def _requestCapabilityGroupParameter(self, host_item):
1360 if host_item is not None: 1361 items = host_item.getNodeItems() 1362 params = [roslib.names.ns_join(item.name, 'capability_group') for item in items if not item.has_configs() and item.is_running() and not host_item.is_in_cap_group(item.name, '', '', 'SYSTEM')] 1363 if params: 1364 self.parameterHandler.requestParameterValues(host_item.masteruri, params)
1365
1366 - def _on_param_values(self, masteruri, code, msg, params):
1367 ''' 1368 Updates the capability groups of nodes from ROS parameter server. 1369 @param masteruri: The URI of the ROS parameter server 1370 @type masteruri: C{str} 1371 @param code: The return code of the request. If not 1, the message is set and the list can be ignored. 1372 @type code: C{int} 1373 @param msg: The message of the result. 1374 @type msg: C{str} 1375 @param params: The dictionary the parameter names and request result. 1376 @type params: C{dict(paramName : (code, statusMessage, parameterValue))} 1377 ''' 1378 host = nm.nameres().address(masteruri) 1379 if host is None: 1380 # try with hostname of the masteruri 1381 host = get_hostname(masteruri) 1382 hostItem = self.get_hostitem(masteruri, host) 1383 changed = False 1384 if hostItem is not None and code == 1: 1385 capabilities = self._set_std_capabilities(hostItem) 1386 available_ns = set(['']) 1387 available_groups = set(['SYSTEM']) 1388 # assumption: all parameter are 'capability_group' parameter 1389 for p, (code_n, _, val) in params.items(): # _:=msg_n 1390 nodename = roslib.names.namespace(p).rstrip(roslib.names.SEP) 1391 ns = roslib.names.namespace(nodename).rstrip(roslib.names.SEP) 1392 if not ns: 1393 ns = roslib.names.SEP 1394 available_ns.add(ns) 1395 if code_n == 1: 1396 # add group 1397 if val: 1398 available_groups.add(val) 1399 if ns not in capabilities: 1400 capabilities[ns] = dict() 1401 if val not in capabilities[ns]: 1402 capabilities[ns][val] = {'images': [], 'nodes': [], 'type': '', 'description': 'This group is created from `capability_group` parameter of the node defined in ROS parameter server.'} 1403 if nodename not in capabilities[ns][val]['nodes']: 1404 capabilities[ns][val]['nodes'].append(nodename) 1405 changed = True 1406 else: 1407 try: 1408 for group, _ in capabilities[ns].items(): 1409 try: 1410 # remove the config from item, if parameter was not foun on the ROS parameter server 1411 groupItem = hostItem.getGroupItem(roslib.names.ns_join(ns, group)) 1412 if groupItem is not None: 1413 nodeItems = groupItem.getNodeItemsByName(nodename, True) 1414 for item in nodeItems: 1415 item.remConfig('') 1416 capabilities[ns][group]['nodes'].remove(nodename) 1417 # remove the group, if empty 1418 if not capabilities[ns][group]['nodes']: 1419 del capabilities[ns][group] 1420 if not capabilities[ns]: 1421 del capabilities[ns] 1422 groupItem.updateDisplayedConfig() 1423 changed = True 1424 except: 1425 pass 1426 except: 1427 pass 1428 # clearup namespaces to remove empty groups 1429 for ns in capabilities.keys(): 1430 if ns and ns not in available_ns: 1431 del capabilities[ns] 1432 changed = True 1433 else: 1434 for group in capabilities[ns].keys(): 1435 if group and group not in available_groups: 1436 del capabilities[ns][group] 1437 changed = True 1438 # update the capabilities and the view 1439 if changed: 1440 if capabilities: 1441 hostItem.addCapabilities('', capabilities, hostItem.masteruri) 1442 hostItem.clearUp() 1443 else: 1444 rospy.logwarn("Error on retrieve \'capability group\' parameter from %s: %s", str(masteruri), msg)
1445
1446 - def set_std_capablilities(self, capabilities):
1447 ''' 1448 Sets the default capabilities description, which is assigned to each new 1449 host. 1450 @param capabilities: the structure for capabilities 1451 @type capabilities: C{dict(namespace: dict(group:dict('type' : str, 'description' : str, 'nodes' : [str])))} 1452 ''' 1453 self._std_capabilities = capabilities
1454
1455 - def addCapabilities(self, masteruri, host_address, cfg, capabilities):
1456 ''' 1457 Adds groups to the model 1458 @param masteruri: ROS master URI 1459 @type masteruri: C{str} 1460 @param host_address: the address the host 1461 @type host_address: C{str} 1462 @param cfg: the configuration name (launch file name or tupel for default configuration) 1463 @type cfg: C{str or (str, str))} 1464 @param capabilities: the structure for capabilities 1465 @type capabilities: C{dict(namespace: dict(group:dict('type' : str, 'description' : str, 'nodes' : [str])))} 1466 ''' 1467 hostItem = self.get_hostitem(masteruri, host_address) 1468 if hostItem is not None: 1469 # add new capabilities 1470 hostItem.addCapabilities(cfg, capabilities, hostItem.masteruri) 1471 self.removeEmptyHosts()
1472
1473 - def appendConfigNodes(self, masteruri, host_address, nodes):
1474 ''' 1475 Adds nodes to the model. If the node is already in the model, only his 1476 configuration list will be extended. 1477 @param masteruri: ROS master URI 1478 @type masteruri: C{str} 1479 @param host_address: the address the host 1480 @type host_address: C{str} 1481 @param nodes: a dictionary with node names and their configurations 1482 @type nodes: C{dict(str : str)} 1483 ''' 1484 hostItem = self.get_hostitem(masteruri, host_address) 1485 if hostItem is not None: 1486 groups = set() 1487 for (name, cfg) in nodes.items(): 1488 items = hostItem.getNodeItemsByName(name) 1489 for item in items: 1490 if item.parent_item is not None: 1491 groups.add(item.parent_item) 1492 # only added the config to the node, if the node is in the same group 1493 if isinstance(item.parent_item, HostItem): 1494 item.addConfig(cfg) 1495 elif hostItem.is_in_cap_group(item.name, cfg, rospy.names.namespace(item.name).rstrip(rospy.names.SEP), item.parent_item.name): 1496 item.addConfig(cfg) 1497 # test for default group 1498 elif hostItem.is_in_cap_group(item.name, '', '', item.parent_item.name): 1499 item.addConfig(cfg) 1500 else: 1501 item.addConfig(cfg) 1502 if not items: 1503 # create the new node 1504 node_info = NodeInfo(name, masteruri) 1505 hostItem.addNode(node_info, cfg) 1506 # get the group of the added node to be able to update the group view, if needed 1507 items = hostItem.getNodeItemsByName(name) 1508 for item in items: 1509 if item.parent_item is not None: 1510 groups.add(item.parent_item) 1511 # update the changed groups 1512 for g in groups: 1513 g.updateDisplayedConfig() 1514 self.removeEmptyHosts()
1515 # update the duplicate state 1516 # self.markNodesAsDuplicateOf(self.getRunningNodes()) 1517
1518 - def removeConfigNodes(self, cfg):
1519 ''' 1520 Removes nodes from the model. If node is running or containing in other 1521 launch or default configurations , only his configuration list will be 1522 reduced. 1523 @param cfg: the name of the confugration to close 1524 @type cfg: C{str} 1525 ''' 1526 for i in reversed(range(self.invisibleRootItem().rowCount())): 1527 host = self.invisibleRootItem().child(i) 1528 items = host.getNodeItems() 1529 groups = set() 1530 for item in items: 1531 removed = item.remConfig(cfg) 1532 if removed and item.parent_item is not None: 1533 groups.add(item.parent_item) 1534 for g in groups: 1535 g.updateDisplayedConfig() 1536 host.remCapablities(cfg) 1537 host.clearUp() 1538 if host.rowCount() == 0: 1539 self.invisibleRootItem().removeRow(i) 1540 elif groups: 1541 # request for all nodes in host the parameter capability_group 1542 self._requestCapabilityGroupParameter(host)
1543
1544 - def removeEmptyHosts(self):
1545 # remove empty hosts 1546 for i in reversed(range(self.invisibleRootItem().rowCount())): 1547 host = self.invisibleRootItem().child(i) 1548 if host.rowCount() == 0 or not host.remote_launched_nodes_updated(): 1549 self.invisibleRootItem().removeRow(i)
1550
1551 - def isDuplicateNode(self, node_name):
1552 for i in reversed(range(self.invisibleRootItem().rowCount())): 1553 host = self.invisibleRootItem().child(i) 1554 if host is not None: # should not occur 1555 nodes = host.getNodeItemsByName(node_name) 1556 for n in nodes: 1557 if n.has_running: 1558 return True 1559 return False
1560
1561 - def getNode(self, node_name, masteruri):
1562 ''' 1563 Since the same node can be included by different groups, this method searches 1564 for all nodes with given name and returns these items. 1565 @param node_name: The name of the node 1566 @type node_name: C{str} 1567 @return: The list with node items. 1568 @rtype: C{[U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}]} 1569 ''' 1570 for i in reversed(range(self.invisibleRootItem().rowCount())): 1571 host = self.invisibleRootItem().child(i) 1572 if host is not None and (masteruri is None or host.masteruri == masteruri): 1573 res = host.getNodeItemsByName(node_name) 1574 if res: 1575 return res 1576 return []
1577
1578 - def getRunningNodes(self):
1579 ''' 1580 Returns a list with all known running nodes. 1581 @rtype: C{[str]} 1582 ''' 1583 running_nodes = list() 1584 # # determine all running nodes 1585 for i in reversed(range(self.invisibleRootItem().rowCount())): 1586 host = self.invisibleRootItem().child(i) 1587 if host is not None: # should not occur 1588 running_nodes[len(running_nodes):] = host.getRunningNodes() 1589 return running_nodes
1590
1591 - def markNodesAsDuplicateOf(self, running_nodes, is_sync_running=False):
1592 ''' 1593 If there are a synchronization running, you have to avoid to running the 1594 node with the same name on different hosts. This method helps to find the 1595 nodes with same name running on other hosts and loaded by a configuration. 1596 The nodes loaded by a configuration will be inform about a currently running 1597 nodes, so a warning can be displayed! 1598 @param running_nodes: The dictionary with names of running nodes and their masteruri 1599 @type running_nodes: C{dict(str:str)} 1600 @param is_sync_running: If the master_sync is running, the nodes are marked 1601 as ghost nodes. So they are handled as running nodes, but has not run 1602 informations. This nodes are running on remote host, but are not 1603 syncronized because of filter or errors. 1604 @type is_sync_running: bool 1605 ''' 1606 for i in reversed(range(self.invisibleRootItem().rowCount())): 1607 host = self.invisibleRootItem().child(i) 1608 if host is not None: # should not occur 1609 host.markNodesAsDuplicateOf(running_nodes, is_sync_running)
1610
1611 - def updateHostDescription(self, masteruri, host, descr_type, descr_name, descr):
1612 ''' 1613 Updates the description of a host. 1614 @param masteruri: ROS master URI of the host to update 1615 @type masteruri: C{str} 1616 @param host: host to update 1617 @type host: C{str} 1618 @param descr_type: the type of the robot 1619 @type descr_type: C{str} 1620 @param descr_name: the name of the robot 1621 @type descr_name: C{str} 1622 @param descr: the description of the robot as a U{http://docutils.sourceforge.net/rst.html|reStructuredText} 1623 @type descr: C{str} 1624 ''' 1625 root = self.invisibleRootItem() 1626 for i in range(root.rowCount()): 1627 if root.child(i) == (unicode(masteruri), unicode(host)): 1628 h = root.child(i) 1629 h.updateDescription(descr_type, descr_name, descr) 1630 return h.updateTooltip()
1631