1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
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
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
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
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
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
145 for ns, groups in capabilities.items():
146 for group, descr in groups.items():
147 group_changed = False
148
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
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
168 for node_name in nodes:
169
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
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
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
202 pass
203
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()
213 try:
214 for cfg, cap in self._capcabilities.items():
215 for ns, groups in cap.items():
216 for group, _ in groups.items():
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
224
225 return result
226
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
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
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
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
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
324 groupItem = self.getGroupItem(group_name)
325 groupItem.addNode(node, cfg)
326 else:
327
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
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
352 groups = self.getGroupItems()
353 for group in groups:
354 group.clearUp(fixed_node_names)
355 removed = False
356
357 for i in reversed(range(self.rowCount())):
358 item = self.child(i)
359 if isinstance(item, NodeItem):
360
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
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
379 removed = True
380 self.removeRow(i)
381 if removed:
382 self.updateIcon()
383
384
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
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
407 items = self.getNodeItemsByName(name)
408 if items:
409 for item in items:
410
411 item.node_info = node
412 else:
413
414 self.addNode(node)
415 if self._has_remote_launched_nodes:
416 self._remote_launched_nodes_updated = True
417 self.clearUp(nodes.keys())
418
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
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
463 if isinstance(self, HostItem):
464
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
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
533
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
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
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
572 '''
573 Updates the configuration representation in other column.
574 '''
575 if self.parent_item is not None:
576
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
587
588
589
590
591
592
593
594
595
596
597
598
599
600
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
614
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
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
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
676
677 @property
680
681 @property
690
691 @property
694
695 @property
697 return self._masteruri
698
699 @property
701 result = nm.nameres().mastername(self._masteruri, self._host)
702 if not result:
703 result = self.hostname
704 return result
705
727
739
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
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
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
783
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
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
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
826
827 STATE_OFF = 0
828 STATE_RUN = 1
829 STATE_WARNING = 2
830 STATE_GHOST = 3
831 STATE_DUPLICATE = 4
832
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
843
844
845
846
847
848
849
850
851 self._cfgs = []
852 self._std_config = None
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
862
863 @property
865 return self._node_info.name
866
867 @property
870
871 @property
873 return self._node_info.publishedTopics
874
875 @property
877 return self._node_info.subscribedTopics
878
879 @property
882
883 @property
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
893 '''
894 Sets the NodeInfo and updates the view, if needed.
895 '''
896 abbos_changed = False
897 run_changed = False
898
899
900
901
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
918 if run_changed and (self.is_running() or self.has_configs) or abbos_changed:
919 self.updateDispayedName()
920
921 if self.parent_item is not None and not isinstance(self.parent_item, HostItem):
922 self.parent_item.updateIcon()
923
924 @property
926 return self._node_info.uri
927
928 @property
930 return self._node_info.pid
931
932 @property
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
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
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
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
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
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
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
1012
1013
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
1030
1031
1032
1033
1034
1035
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
1063
1064
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
1076 '''
1077 Returns the list with all launch configurations assigned to this item.
1078 @rtype: C{[str]}
1079 '''
1080 return self._cfgs
1081
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
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
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
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
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
1146
1147
1148
1151
1152 @classmethod
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
1170
1171 return items
1172
1174 return not (len(self._cfgs) == 0)
1175
1177 return not (self._node_info.pid is None and self._node_info.uri is None)
1178
1180 return self._std_config == ''
1181
1182 @classmethod
1188
1189 @classmethod
1195
1196 @classmethod
1198 return isinstance(cfg, tuple)
1199
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
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
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
1231
1232
1233
1234
1235
1236
1237 header = [('Name', 450),
1238 ('Cfgs', -1)]
1239
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
1265 self.parameterHandler = ParameterHandler()
1266
1267 self.parameterHandler.parameter_values_signal.connect(self._on_param_values)
1268
1269
1270 @property
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
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
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
1304 local = (self.local_addr in [address] + nm.nameres().resolve_cached(address) and
1305 self._local_masteruri == masteruri)
1306
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
1356
1357
1358
1365
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
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
1389 for p, (code_n, _, val) in params.items():
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
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
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
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
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
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
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
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
1470 hostItem.addCapabilities(cfg, capabilities, hostItem.masteruri)
1471 self.removeEmptyHosts()
1472
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
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
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
1504 node_info = NodeInfo(name, masteruri)
1505 hostItem.addNode(node_info, cfg)
1506
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
1512 for g in groups:
1513 g.updateDisplayedConfig()
1514 self.removeEmptyHosts()
1515
1516
1517
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
1542 self._requestCapabilityGroupParameter(host)
1543
1545
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
1552 for i in reversed(range(self.invisibleRootItem().rowCount())):
1553 host = self.invisibleRootItem().child(i)
1554 if host is not None:
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
1579 '''
1580 Returns a list with all known running nodes.
1581 @rtype: C{[str]}
1582 '''
1583 running_nodes = list()
1584
1585 for i in reversed(range(self.invisibleRootItem().rowCount())):
1586 host = self.invisibleRootItem().child(i)
1587 if host is not None:
1588 running_nodes[len(running_nodes):] = host.getRunningNodes()
1589 return running_nodes
1590
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:
1609 host.markNodesAsDuplicateOf(running_nodes, is_sync_running)
1610
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