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