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