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()
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
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 if self._capcabilities.has_key(config):
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, descr 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 c, 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())
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 tooltip = ''
700 if self.descr_type or self.descr_name or self.descr:
701 tooltip += '<h4>%s</h4><dl>'%self.descr_name
702 if self.descr_type:
703 tooltip += '<dt>Type: %s</dt></dl>'%self.descr_type
704 if extended:
705 try:
706 from docutils import examples
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())
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 from docutils import examples
733 tooltip += examples.html_body('- %s'%('\n- '.join(capabilities)), input_encoding='utf8')
734 except:
735 import traceback
736 rospy.logwarn("Error while generate description for a tooltip: %s", traceback.format_exc())
737 return '<div>%s</div>'%tooltip if tooltip else ''
738
741
743 '''
744 Compares the address of the host.
745 '''
746 if isinstance(item, str) or isinstance(item, unicode):
747 return str(self.id).lower() == item.lower()
748 elif isinstance(item, tuple):
749 return str(self.id).lower() == str(item).lower()
750 elif isinstance(item, HostItem):
751 return str(self.id).lower() == str(item.id).lower()
752 return False
753
755 '''
756 Compares the address of the host.
757 '''
758 if isinstance(item, str) or isinstance(item, unicode):
759 return str(self.id).lower() > item.lower()
760 elif isinstance(item, tuple):
761 return str(self.id).lower() > str(item).lower()
762 elif isinstance(item, HostItem):
763 return str(self.id).lower() > str(item.id).lower()
764 return False
765
766
767
768
769
770
771 -class NodeItem(QtGui.QStandardItem):
772 '''
773 The NodeItem stores the information about the node using the ExtendedNodeInfo
774 class and represents it in a L{PySide.QtGui.QTreeModel} using the
775 L{PySide.QtGui.QStandardItemModel}
776 '''
777
778 ITEM_TYPE = QtGui.QStandardItem.UserType + 35
779 NAME_ROLE = QtCore.Qt.UserRole + 1
780 COL_CFG = 1
781
782
783 STATE_OFF = 0
784 STATE_RUN = 1
785 STATE_WARNING = 2
786 STATE_GHOST = 3
787 STATE_DUPLICATE = 4
788
790 '''
791 Initialize the NodeItem instance.
792 @param node_info: the node information
793 @type node_info: L{master_discovery_fkie.NodeInfo}
794 '''
795 QtGui.QStandardItem.__init__(self, node_info.name)
796 self.parent_item = None
797 self._node_info = node_info.copy()
798
799
800
801
802
803
804
805
806
807 self._cfgs = []
808 self._std_config = None
809 self._is_ghost = False
810 self._has_running = False
811 self.setIcon(QtGui.QIcon(':/icons/state_off.png'))
812 self._state = NodeItem.STATE_OFF
813
814 @property
817
818 @property
820 return self._node_info.name
821
822 @property
825
826 @property
828 return self._node_info.publishedTopics
829
830 @property
832 return self._node_info.subscribedTopics
833
834 @property
837
838 @property
840 '''
841 Returns the NodeInfo instance of this node.
842 @rtype: L{master_discovery_fkie.NodeInfo}
843 '''
844 return self._node_info
845
846 @node_info.setter
848 '''
849 Sets the NodeInfo and updates the view, if needed.
850 '''
851 abbos_changed = False
852 run_changed = False
853
854
855
856
857 if self._node_info.publishedTopics != node_info.publishedTopics:
858 abbos_changed = True
859 self._node_info._publishedTopics = list(node_info.publishedTopics)
860 if self._node_info.subscribedTopics != node_info.subscribedTopics:
861 abbos_changed = True
862 self._node_info._subscribedTopics = list(node_info.subscribedTopics)
863 if self._node_info.services != node_info.services:
864 abbos_changed = True
865 self._node_info._services = list(node_info.services)
866 if self._node_info.pid != node_info.pid:
867 self._node_info.pid = node_info.pid
868 run_changed = True
869 if self._node_info.uri != node_info.uri:
870 self._node_info.uri = node_info.uri
871 run_changed = True
872
873 if run_changed and (self.is_running() or self.has_configs) or abbos_changed:
874 self.updateDispayedName()
875
876 if not self.parent_item is None and not isinstance(self.parent_item, HostItem):
877 self.parent_item.updateIcon()
878
879 @property
881 return self._node_info.uri
882
883 @property
885 return self._node_info.pid
886
887 @property
889 '''
890 Returns C{True}, if there are exists other nodes with the same name. This
891 variable must be set manually!
892 @rtype: C{bool}
893 '''
894 return self._has_running
895
896 @has_running.setter
898 '''
899 Sets however other node with the same name are running or not (on other hosts)
900 and updates the view of this item.
901 '''
902 if self._has_running != state:
903 self._has_running = state
904 if self.has_configs() or self.is_running():
905 self.updateDispayedName()
906 if not self.parent_item is None and not isinstance(self.parent_item, HostItem):
907 self.parent_item.updateIcon()
908
909 @property
911 '''
912 Returns C{True}, if there are exists other runnig nodes with the same name. This
913 variable must be set manually!
914 @rtype: C{bool}
915 '''
916 return self._is_ghost
917
918 @is_ghost.setter
920 '''
921 Sets however other node with the same name is running (on other hosts) and
922 and the host showing this node the master_sync is running, but the node is
923 not synchronized.
924 '''
925 if self._is_ghost != state:
926 self._is_ghost = state
927 if self.has_configs() or self.is_running():
928 self.updateDispayedName()
929 if not self.parent_item is None and not isinstance(self.parent_item, HostItem):
930 self.parent_item.updateIcon()
931
932 - def data(self, role):
933 if role == self.NAME_ROLE:
934 return self.name
935 else:
936 return QtGui.QStandardItem.data(self, role)
937
939 '''
940 Updates the name representation of the Item
941 '''
942 tooltip = '<h4>%s</h4><dl>'%self.node_info.name
943 tooltip += '<dt><b>URI:</b> %s</dt>'%self.node_info.uri
944 tooltip += '<dt><b>PID:</b> %s</dt>'%self.node_info.pid
945 tooltip += '<dt><b>ORG.MASTERURI:</b> %s</dt></dl>'%self.node_info.masteruri
946
947 master_discovered = nm.nameres().hasMaster(self.node_info.masteruri)
948 local = False
949
950
951 if not self.node_info.pid is None:
952 self._state = NodeItem.STATE_RUN
953 self.setIcon(QtGui.QIcon(':/icons/state_run.png'))
954
955 self.setToolTip('')
956 elif not self.node_info.uri is None and not self.node_info.isLocal:
957 self._state = NodeItem.STATE_RUN
958 self.setIcon(QtGui.QIcon(':/icons/state_unknown.png'))
959 tooltip += '<dl><dt>(Remote nodes will not be ping, so they are always marked running)</dt></dl>'
960 tooltip += '</dl>'
961 self.setToolTip('<div>%s</div>'%tooltip)
962
963
964
965
966
967
968
969 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):
970 self.setIcon(QtGui.QIcon(':/icons/crystal_clear_warning.png'))
971 self._state = NodeItem.STATE_WARNING
972 tooltip += '<dl><dt>Can\'t get node contact information, but there exists publisher, subscriber or services of this node.</dt></dl>'
973 tooltip += '</dl>'
974 self.setToolTip('<div>%s</div>'%tooltip)
975 elif not self.node_info.uri is None:
976 self._state = NodeItem.STATE_WARNING
977 self.setIcon(QtGui.QIcon(':/icons/crystal_clear_warning.png'))
978 if not self.node_info.isLocal and master_discovered:
979 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
980 self.setToolTip('<div>%s</div>'%tooltip)
981 elif self.is_ghost:
982 self._state = NodeItem.STATE_GHOST
983 self.setIcon(QtGui.QIcon(':/icons/state_ghost.png'))
984 tooltip = '<h4>The node is running, but not synchronized because of filter or errors, see master_sync log.</h4>'
985 self.setToolTip('<div>%s</div>'%tooltip)
986 elif self.has_running:
987 self._state = NodeItem.STATE_DUPLICATE
988 self.setIcon(QtGui.QIcon(':/icons/imacadam_stop.png'))
989 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>'
990 self.setToolTip('<div>%s</div>'%tooltip)
991 else:
992 self._state = NodeItem.STATE_OFF
993 self.setIcon(QtGui.QIcon(':/icons/state_off.png'))
994 self.setToolTip('')
995
996
997
998
999
1001 '''
1002 Updates the URI representation in other column.
1003 '''
1004 if not self.parent_item is None:
1005 uri_col = self.parent_item.child(self.row(), NodeItem.COL_URI)
1006 if not uri_col is None and isinstance(uri_col, QtGui.QStandardItem):
1007 uri_col.setText(str(self.node_info.uri) if not self.node_info.uri is None else "")
1008
1009 @property
1011 '''
1012 Returns the list with all launch configurations assigned to this item.
1013 @rtype: C{[str]}
1014 '''
1015 return self._cfgs
1016
1018 '''
1019 Add the given configurations to the node.
1020 @param cfg: the loaded configuration, which contains this node.
1021 @type cfg: C{str}
1022 '''
1023 if cfg == '':
1024 self._std_config = cfg
1025 elif cfg and not cfg in self._cfgs:
1026 self._cfgs.append(cfg)
1027 self.updateDisplayedConfig()
1028
1030 '''
1031 Remove the given configurations from the node.
1032 @param cfg: the loaded configuration, which contains this node.
1033 @type cfg: C{str}
1034 '''
1035 result = False
1036 if cfg == '':
1037 self._std_config = None
1038 result = True
1039 if cfg in self._cfgs:
1040 self._cfgs.remove(cfg)
1041 result = True
1042 if result and (self.has_configs() or self.is_running()):
1043 self.updateDisplayedConfig()
1044 return result
1045
1047 '''
1048 Updates the configuration representation in other column.
1049 '''
1050 if not self.parent_item is None:
1051 cfg_col = self.parent_item.child(self.row(), NodeItem.COL_CFG)
1052 if not cfg_col is None and isinstance(cfg_col, QtGui.QStandardItem):
1053 cfg_count = len(self._cfgs)
1054 cfg_col.setText(str(''.join(['[',str(cfg_count),']'])) if cfg_count > 1 else "")
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070 has_launches = NodeItem.has_launch_cfgs(self._cfgs)
1071 has_defaults = NodeItem.has_default_cfgs(self._cfgs)
1072 if has_launches and has_defaults:
1073 cfg_col.setIcon(QtGui.QIcon(':/icons/crystal_clear_launch_file_def_cfg.png'))
1074 elif has_launches:
1075 cfg_col.setIcon(QtGui.QIcon(':/icons/crystal_clear_launch_file.png'))
1076 elif has_defaults:
1077 cfg_col.setIcon(QtGui.QIcon(':/icons/default_cfg.png'))
1078 else:
1079 cfg_col.setIcon(QtGui.QIcon())
1080
1081
1082
1083
1086
1087 @classmethod
1089 '''
1090 Creates a new node row and returns it as a list with items. This list is
1091 used for the visualization of node data as a table row.
1092 @param name: the node name
1093 @type name: C{str}
1094 @param masteruri: the URI or the ROS master assigned to this node.
1095 @type masteruri: C{str}
1096 @return: the list for the representation as a row
1097 @rtype: C{[L{NodeItem}, L{PySide.QtGui.QStandardItem}(Cofigurations), L{PySide.QtGui.QStandardItem}(Node URI)]}
1098 '''
1099 items = []
1100 item = NodeItem(NodeInfo(name, masteruri))
1101 items.append(item)
1102 cfgitem = QtGui.QStandardItem()
1103 items.append(cfgitem)
1104
1105
1106 return items
1107
1109 return not (len(self._cfgs) == 0)
1110
1112 return not (self._node_info.pid is None and self._node_info.uri is None)
1113
1115 return self._std_config == ''
1116
1117 @classmethod
1123
1124 @classmethod
1130
1131 @classmethod
1133 return isinstance(cfg, tuple)
1134
1136 '''
1137 Compares the name of the node.
1138 '''
1139 if isinstance(item, str) or isinstance(item, unicode):
1140 return self.name == item
1141 elif not (item is None):
1142 return self.name == item.name
1143 return False
1144
1146 '''
1147 Compares the name of the node.
1148 '''
1149 if isinstance(item, str) or isinstance(item, unicode):
1150 return self.name > item
1151 elif not (item is None):
1152 return self.name > item.name
1153 return False
1154
1155
1156
1157
1158
1159
1160 -class NodeTreeModel(QtGui.QStandardItemModel):
1161 '''
1162 The model to show the nodes running in a ROS system or loaded by a launch
1163 configuration.
1164 '''
1165
1166
1167
1168
1169
1170
1171
1172 header = [('Name', 450),
1173 ('Cfgs', -1)]
1174
1175
1176 hostInserted = QtCore.Signal(HostItem)
1177 '''@ivar: the Qt signal, which is emitted, if a new host was inserted.
1178 Parameter: L{QtCore.QModelIndex} of the inserted host item'''
1179
1180 - def __init__(self, host_address, masteruri, parent=None):
1181 '''
1182 Initialize the model.
1183 '''
1184 super(NodeTreeModel, self).__init__(parent)
1185 self.setColumnCount(len(NodeTreeModel.header))
1186 self.setHorizontalHeaderLabels([label for label, width in NodeTreeModel.header])
1187 self._local_host_address = host_address
1188 self._std_capabilities = {'': {'SYSTEM': {'images': [],
1189 'nodes': [ '/rosout',
1190 '/master_discovery',
1191 '/zeroconf',
1192 '/master_sync',
1193 '/node_manager',
1194 '/dynamic_reconfigure/*'],
1195 'type': '',
1196 'description': 'This group contains the system management nodes.'} } }
1197
1198
1199 self.parameterHandler = ParameterHandler()
1200
1201 self.parameterHandler.parameter_values_signal.connect(self._on_param_values)
1202
1203
1204
1205 @property
1207 return self._local_host_address
1208
1209 - def flags(self, index):
1210 if not index.isValid():
1211 return QtCore.Qt.NoItemFlags
1212 return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
1213
1215 if not host_item is None:
1216 cap = self._std_capabilities
1217 s = roslib.names.SEP
1218 hostname = roslib.names.SEP.join(['', host_item.hostname, '*', 'default_cfg'])
1219 if not hostname in cap['']['SYSTEM']['nodes']:
1220 cap['']['SYSTEM']['nodes'].append(hostname)
1221 host_item.addCapabilities('', cap, host_item.masteruri)
1222 return cap
1223 return dict(self._std_capabilities)
1224
1226 '''
1227 Searches for the host item in the model. If no item is found a new one will
1228 created and inserted in sorted order.
1229 @param masteruri: ROS master URI
1230 @type masteruri: C{str}
1231 @param address: the address of the host
1232 @type address: C{str}
1233 @return: the item associated with the given master
1234 @rtype: L{HostItem}
1235 '''
1236 if masteruri is None:
1237 return None
1238 host = (unicode(masteruri), unicode(address))
1239 local = (self.local_addr == host)
1240
1241 root = self.invisibleRootItem()
1242 for i in range(root.rowCount()):
1243 if root.child(i) == host:
1244 return root.child(i)
1245 elif root.child(i) > host:
1246 hostItem = HostItem(masteruri, address, local)
1247 self.insertRow(i, hostItem)
1248 self.hostInserted.emit(hostItem)
1249 self._set_std_capabilities(hostItem)
1250 return hostItem
1251 hostItem = HostItem(masteruri, address, local)
1252 self.appendRow(hostItem)
1253 self.hostInserted.emit(hostItem)
1254 self._set_std_capabilities(hostItem)
1255 return hostItem
1256
1258 '''
1259 Updates the model data.
1260 @param nodes: a dictionary with name and info objects of the nodes.
1261 @type nodes: C{dict(str:L{NodeInfo}, ...)}
1262 '''
1263
1264 hosts = dict()
1265 for (name, node) in nodes.items():
1266 addr = nm.nameres().getHostname(node.uri if not node.uri is None else node.masteruri)
1267 host = (node.masteruri, addr)
1268 if not hosts.has_key(host):
1269 hosts[host] = dict()
1270 hosts[host][name] = node
1271
1272 for ((masteruri, host), nodes_filtered) in hosts.items():
1273 hostItem = self.getHostItem(masteruri, host)
1274
1275 if not hostItem is None:
1276 hostItem.updateRunningNodeState(nodes_filtered)
1277
1278 for i in reversed(range(self.invisibleRootItem().rowCount())):
1279 host = self.invisibleRootItem().child(i)
1280 if not hosts.has_key(host.id):
1281 host.updateRunningNodeState({})
1282 self.removeEmptyHosts()
1283
1284 for ((masteruri, host), nodes_filtered) in hosts.items():
1285 hostItem = self.getHostItem(masteruri, host)
1286 self._requestCapabilityGroupParameter(hostItem)
1287
1288
1289
1296
1298 '''
1299 Updates the capability groups of nodes from ROS parameter server.
1300 @param masteruri: The URI of the ROS parameter server
1301 @type masteruri: C{str}
1302 @param code: The return code of the request. If not 1, the message is set and the list can be ignored.
1303 @type code: C{int}
1304 @param msg: The message of the result.
1305 @type msg: C{str}
1306 @param params: The dictionary the parameter names and request result.
1307 @type param: C{dict(paramName : (code, statusMessage, parameterValue))}
1308 '''
1309 host = nm.nameres().address(masteruri)
1310 hostItem = self.getHostItem(masteruri, host)
1311 changed = False
1312 if not hostItem is None and code == 1:
1313 capabilities = self._set_std_capabilities(hostItem)
1314 available_ns = set([''])
1315 available_groups = set(['SYSTEM'])
1316
1317 for p, (code_n, msg_n, val) in params.items():
1318 nodename = roslib.names.namespace(p).rstrip(roslib.names.SEP)
1319 ns = roslib.names.namespace(nodename).rstrip(roslib.names.SEP)
1320 available_ns.add(ns)
1321 if code_n == 1:
1322
1323 if val:
1324 available_groups.add(val)
1325 if not capabilities.has_key(ns):
1326 capabilities[ns] = dict()
1327 if not capabilities[ns].has_key(val):
1328 capabilities[ns][val] = {'images': [], 'nodes': [], 'type': '', 'description': 'This group is created from `capability_group` parameter of the node defined in ROS parameter server.' }
1329 if not nodename in capabilities[ns][val]['nodes']:
1330 capabilities[ns][val]['nodes'].append(nodename)
1331 changed = True
1332 else:
1333 try:
1334 for group, caps in capabilities[ns].items():
1335 try:
1336
1337 groupItem = hostItem.getGroupItem(roslib.names.ns_join(ns,group))
1338 if not groupItem is None:
1339 nodeItems = groupItem.getNodeItemsByName(nodename, True)
1340 for item in nodeItems:
1341 item.remConfig('')
1342 capabilities[ns][group]['nodes'].remove(nodename)
1343
1344 if not capabilities[ns][group]['nodes']:
1345 del capabilities[ns][group]
1346 if not capabilities[ns]:
1347 del capabilities[ns]
1348 groupItem.updateDisplayedConfig()
1349 changed = True
1350 except:
1351 pass
1352 except:
1353 pass
1354
1355 for ns in capabilities.keys():
1356 if ns and not ns in available_ns:
1357 del capabilities[ns]
1358 changed = True
1359 else:
1360 for group in capabilities[ns].keys():
1361 if group and not group in available_groups:
1362 del capabilities[ns][group]
1363 changed = True
1364
1365 if changed:
1366 if capabilities:
1367 hostItem.addCapabilities('', capabilities, hostItem.masteruri)
1368 hostItem.clearUp()
1369 else:
1370 rospy.logwarn("Error on retrieve \'capability group\' parameter from %s: %s", str(masteruri), msg)
1371
1372
1374 '''
1375 Sets the default capabilities description, which is assigned to each new
1376 host.
1377 @param capabilities: the structure for capabilities
1378 @type capabilities: C{dict(namespace: dict(group:dict('type' : str, 'description' : str, 'nodes' : [str])))}
1379 '''
1380 self._std_capabilities = capabilities
1381
1383 '''
1384 Adds groups to the model
1385 @param masteruri: ROS master URI
1386 @type masteruri: C{str}
1387 @param host_address: the address the host
1388 @type host_address: C{str}
1389 @param cfg: the configuration name (launch file name or tupel for default configuration)
1390 @type cfg: C{str or (str, str))}
1391 @param capabilities: the structure for capabilities
1392 @type capabilities: C{dict(namespace: dict(group:dict('type' : str, 'description' : str, 'nodes' : [str])))}
1393 '''
1394 hostItem = self.getHostItem(masteruri, host_address)
1395 if not hostItem is None:
1396
1397 hostItem.addCapabilities(cfg, capabilities, hostItem.masteruri)
1398 self.removeEmptyHosts()
1399
1401 '''
1402 Adds nodes to the model. If the node is already in the model, only his
1403 configuration list will be extended.
1404 @param masteruri: ROS master URI
1405 @type masteruri: C{str}
1406 @param host_address: the address the host
1407 @type host_address: C{str}
1408 @param nodes: a dictionary with node names and their configurations
1409 @type nodes: C{dict(str : str)}
1410 '''
1411 hostItem = self.getHostItem(masteruri, host_address)
1412 if not hostItem is None:
1413 groups = set()
1414 for (name, cfg) in nodes.items():
1415 items = hostItem.getNodeItemsByName(name)
1416 for item in items:
1417 if not item.parent_item is None:
1418 groups.add(item.parent_item)
1419
1420 if isinstance(item.parent_item, HostItem):
1421 item.addConfig(cfg)
1422 elif hostItem.is_in_cap_group(item.name, cfg, rospy.names.namespace(item.name).rstrip(rospy.names.SEP), item.parent_item.name):
1423 item.addConfig(cfg)
1424
1425 elif hostItem.is_in_cap_group(item.name, '', '', item.parent_item.name):
1426 item.addConfig(cfg)
1427 else:
1428 item.addConfig(cfg)
1429 if not items:
1430
1431 node_info = NodeInfo(name, masteruri)
1432 hostItem.addNode(node_info, cfg)
1433
1434 items = hostItem.getNodeItemsByName(name)
1435 for item in items:
1436 if not item.parent_item is None:
1437 groups.add(item.parent_item)
1438
1439 for g in groups:
1440 g.updateDisplayedConfig()
1441 self.removeEmptyHosts()
1442
1443
1444
1446 '''
1447 Removes nodes from the model. If node is running or containing in other
1448 launch or default configurations , only his configuration list will be
1449 reduced.
1450 @param cfg: the name of the confugration to close
1451 @type cfg: C{str}
1452 '''
1453 for i in reversed(range(self.invisibleRootItem().rowCount())):
1454 host = self.invisibleRootItem().child(i)
1455 items = host.getNodeItems()
1456 groups = set()
1457 for item in items:
1458 removed = item.remConfig(cfg)
1459 if removed and not item.parent_item is None:
1460 groups.add(item.parent_item)
1461 for g in groups:
1462 g.updateDisplayedConfig()
1463 host.remCapablities(cfg)
1464 host.clearUp()
1465 if host.rowCount() == 0:
1466 self.invisibleRootItem().removeRow(i)
1467 elif groups:
1468
1469 self._requestCapabilityGroupParameter(host)
1470
1472
1473 for i in reversed(range(self.invisibleRootItem().rowCount())):
1474 host = self.invisibleRootItem().child(i)
1475 if host.rowCount() == 0:
1476 self.invisibleRootItem().removeRow(i)
1477
1479 for i in reversed(range(self.invisibleRootItem().rowCount())):
1480 host = self.invisibleRootItem().child(i)
1481 if not host is None:
1482 nodes = host.getNodeItemsByName(node_name)
1483 for n in nodes:
1484 if n.has_running:
1485 return True
1486 return False
1487
1489 '''
1490 Since the same node can be included by different groups, this method searches
1491 for all nodes with given name and returns these items.
1492 @param node_name: The name of the node
1493 @type node_name: C{str}
1494 @return: The list with node items.
1495 @rtype: C{[L{PySide.QtGui.QStandardItem}]}
1496 '''
1497 for i in reversed(range(self.invisibleRootItem().rowCount())):
1498 host = self.invisibleRootItem().child(i)
1499 if not host is None:
1500 return host.getNodeItemsByName(node_name)
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