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 QObject, Qt, Signal
34 from python_qt_binding.QtGui import QIcon, QStandardItem, QStandardItemModel
35 try:
36 from python_qt_binding.QtGui import QPushButton
37 except:
38 from python_qt_binding.QtWidgets import QPushButton
39 from socket import getaddrinfo, AF_INET6
40 import threading
41
42 from master_discovery_fkie.common import get_hostname
43 import node_manager_fkie as nm
105
108 '''
109 This object is needed to insert into the QStandardModel.
110 '''
111 ITEM_TYPE = QStandardItem.UserType + 35
112
118
119 @property
121 return self.button.master()
122
123 @property
126
127 @synchronized.setter
130
132 return self.button == item
133
135 return self.button > item
136
139 '''
140 The master item stored in the master model. This class stores the master as
141 master_discovery_fkie.ROSMaster.
142 '''
143
144 ITEM_TYPE = QStandardItem.UserType + 34
145
146 - def __init__(self, master, local=False, quality=None, parent=None):
147 self.name = ''.join([master.name, ' (localhost)']) if local else master.name
148 QStandardItem.__init__(self, self.name)
149 self.parent_item = None
150 self._master = master
151 self.local = local
152 self.__quality = quality
153 self.descr = ''
154 self.ICONS = {'green': QIcon(":/icons/stock_connect_green.png"),
155 'yellow': QIcon(":/icons/stock_connect_yellow.png"),
156 'red': QIcon(":/icons/stock_connect_red.png"),
157 'grey': QIcon(":/icons/stock_connect.png"),
158 'disconnected': QIcon(":/icons/stock_disconnect.png"),
159 'warning': QIcon(':/icons/crystal_clear_warning.png'),
160 'clock_warn': QIcon(':/icons/crystal_clear_xclock_fail.png')}
161 self.master_ip = None
162 self._master_errors = []
163 self._timediff = 0
164 self._threaded_get_ip()
165 self.updateNameView(master, quality, self)
166
168 thread = threading.Thread(target=self.__get_ip)
169 thread.daemon = True
170 thread.start()
171
173 try:
174
175 result = getaddrinfo(get_hostname(self.master.uri), None)
176 ips = []
177 for r in result:
178 if r[0] == AF_INET6:
179 (_family, _socktype, _proto, _canonname, (ip, _port, _flow, _scope)) = r
180 else:
181 (_family, _socktype, _proto, _canonname, (ip, _port)) = r
182 if self.master_ip is None and ip:
183 self.master_ip = ''
184 if ip and ip not in ips:
185 self.master_ip = ' '.join([self.master_ip, ip])
186 ips.append(ip)
187
188 except:
189 import traceback
190 print traceback.format_exc(1)
191
192 @property
195
196 @master.setter
199
200 @property
202 return self.__quality
203
204 @quality.setter
209
213
217
219 '''
220 This method is called after the master state is changed to update the
221 representation of the master. The name will not be changed, but all other
222 data.
223 @param parent: Item which contains this master item. This is needed to update
224 other columns of this master.
225 @type parent: U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}
226 '''
227 if parent is not None:
228
229 child = parent.child(self.row(), MasterModel.COL_NAME)
230 if child is not None:
231 self.updateNameView(self.master, self.quality, child)
232
234 '''
235 Updates the representation of the column contains the name state.
236 @param master: the topic data
237 @type master: master_discovery_fkie.TopicInfo
238 @param item: corresponding item in the model
239 @type item: L{TopicItem}
240 '''
241 tooltip = ''.join(['<html><body>'])
242 tooltip = ''.join([tooltip, '<h4>', master.uri, '</h4>'])
243 tooltip = ''.join([tooltip, '<dl>'])
244 tooltip = ''.join([tooltip, '<dt>', 'IP: ', str(self.master_ip), '</dt>'])
245 if master.online:
246 if quality is not None and quality != -1.:
247 tooltip = ''.join([tooltip, '<dt>', 'Quality: ', str(quality), ' %', '</dt>'])
248 else:
249 tooltip = ''.join([tooltip, '<dt>', 'Quality: not available</dt>'])
250 else:
251 tooltip = ''.join([tooltip, '<dt>', 'offline', '</dt>'])
252 tooltip = ''.join([tooltip, '</dl>'])
253 if item.descr:
254 tooltip = ''.join([tooltip, item.descr])
255
256 if master.online:
257 timediff = abs(self._timediff) > nm.settings().max_timediff
258 if self._master_errors or self.master_ip is None or timediff:
259 item.setIcon(self.ICONS['warning'])
260 if timediff:
261 tooltip = ''.join([tooltip, '<h4>', '<font color="#CC0000">Time difference to the host is about %.3f seconds!</font>' % self._timediff, '</h4>'])
262 item.setIcon(self.ICONS['clock_warn'])
263 if self.master_ip is None:
264 tooltip = ''.join([tooltip, '<h4>', '<font color="#CC0000">Host not reachable by name!!! The ROS topics may not by connected!!!</font>', '</h4>'])
265 if self._master_errors:
266 tooltip = ''.join([tooltip, '<h4>Errors reported by master_discovery:</h4>'])
267 for err in self._master_errors:
268 tooltip = ''.join([tooltip, '<dt><font color="#CC0000">%s</font></dt>' % err])
269 elif quality is not None and quality != -1.:
270 if quality > 30:
271 item.setIcon(self.ICONS['green'])
272 elif quality > 5:
273 item.setIcon(self.ICONS['yellow'])
274 else:
275 item.setIcon(self.ICONS['red'])
276 else:
277 item.setIcon(self.ICONS['grey'])
278 else:
279 item.setIcon(self.ICONS['disconnected'])
280
281 tooltip = ''.join([tooltip, '</body></html>'])
282 item.setToolTip(tooltip)
283
287
288 @classmethod
290 '''
291 @param text: the text
292 @type text: C{str}
293 @return: the HTML representation of the name of the text
294 @rtype: C{str}
295 '''
296 ns, sep, name = text.rpartition('/')
297 result = ''
298 if sep:
299 result = ''.join(['<html><body>', '<span style="color:gray;">', str(ns), sep, '</span><b>', name, '</b></body></html>'])
300 else:
301 result = name
302 return result
303
306
308 if isinstance(item, str) or isinstance(item, unicode):
309 return self.master.name.lower() == item.lower()
310 elif not (item is None):
311 return self.master.name.lower() == item.master.name.lower()
312 return False
313
315 if isinstance(item, str) or isinstance(item, unicode):
316 local = False
317 try:
318 local = nm.is_local(item)
319 except:
320 pass
321 if self.local and not local:
322 return False
323 return self.master.name.lower() > item.lower()
324 elif not (item is None):
325 if self.local and not item.local:
326 return False
327 return self.master.name.lower() > item.master.name.lower()
328 return False
329
332 '''
333 The model to manage the list with masters in ROS network.
334 '''
335 sync_start = Signal(str)
336 sync_stop = Signal(str)
337
338 header = [('Sync', 28), ('Name', -1)]
339 '''@ivar: the list with columns C{[(name, width), ...]}'''
340 COL_SYNC = 0
341 COL_NAME = 1
342 COL_SYNCBTN = 2
343
344 - def __init__(self, local_masteruri=None):
345 '''
346 Creates a new list model.
347 '''
348 QStandardItemModel.__init__(self)
349 self.setColumnCount(len(MasterModel.header))
350 self._masteruri = local_masteruri
351 self.parent_view = None
352 self.pyqt_workaround = dict()
353
355 '''
356 @param index: parent of the list
357 @type index: U{QtCore.QModelIndex<https://srinikom.github.io/pyside-docs/PySide/QtCore/QModelIndex.html>}
358 @return: Flag or the requestet item
359 @rtype: U{QtCore.Qt.ItemFlag<https://srinikom.github.io/pyside-docs/PySide/QtCore/Qt.html>}
360 @see: U{http://www.pyside.org/docs/pyside-1.0.1/PySide/QtCore/Qt.html}
361 '''
362 if not index.isValid():
363 return Qt.NoItemFlags
364
365
366 return Qt.ItemIsSelectable | Qt.ItemIsEnabled
367
368
370 '''
371 Updates the information of the ros master. If the ROS master not exists, it
372 will be added.
373
374 @param master: the ROS master to update
375 @type master: U{master_discovery_fkie.msg.ROSMaster<http://docs.ros.org/api/multimaster_msgs_fkie/html/msg/ROSMaster.html>}
376 '''
377
378 root = self.invisibleRootItem()
379 for i in reversed(range(root.rowCount())):
380 masterItem = root.child(i)
381 if masterItem.master.uri == master.uri and masterItem.master.name != master.name:
382 root.removeRow(i)
383 try:
384 del self.pyqt_workaround[masterItem.master.name]
385 except:
386 pass
387 break
388
389
390 root = self.invisibleRootItem()
391 doAddItem = True
392 is_local = nm.is_local(get_hostname(master.uri))
393 for index in range(root.rowCount()):
394 masterItem = root.child(index, self.COL_NAME)
395 if (masterItem == master.name):
396
397 masterItem.master = master
398 masterItem.updateMasterView(root)
399 doAddItem = False
400 break
401 elif (masterItem > master.name):
402 self.addRow(master, is_local, root, index)
403 doAddItem = False
404 break
405 if doAddItem:
406 self.addRow(master, is_local, root, -1)
407
408 - def addRow(self, master, local, root, index):
409 '''
410 Creates the list of the items from master. This list is used for the
411 visualization of master data as a table row.
412 @param master: the master data
413 @type master: master_discovery_fkie.ROSMaster
414 @param local: whether the master is local or not
415 @type local: bool
416 @return: the list for the representation as a row
417 @rtype: C{[L{MasterItem} or U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}, ...]}
418 '''
419 items = []
420 sync_item = MasterSyncItem(master)
421 items.append(sync_item)
422 name_item = MasterItem(master, local)
423 items.append(name_item)
424 name_item.parent_item = root
425 self.pyqt_workaround[master.name] = items
426
427 if index > -1:
428 root.insertRow(index, items)
429 else:
430 root.appendRow(items)
431
432 if self.parent_view is not None:
433 newindex = index if index > -1 else root.rowCount() - 1
434 self.parent_view.setIndexWidget(self.index(newindex, self.COL_SYNC), sync_item.button.widget)
435 sync_item.button.clicked.connect(self.on_sync_clicked)
436 return items
437
439 '''
440 Updates the information of the ros master.
441
442 @param master: the ROS master to update
443 @type master: C{str}
444 @param quality: the quality of the connection to master
445 @type quality: C{float}
446 '''
447 root = self.invisibleRootItem()
448 for i in reversed(range(root.rowCount())):
449 masterItem = root.child(i, self.COL_NAME)
450 if masterItem.master.name in master:
451 masterItem.quality = quality
452 break
453
469
471 '''
472 Remove the master with given name.
473
474 @param master: the ROS master to add
475 @type master: C{str}
476 '''
477 root = self.invisibleRootItem()
478 for i in reversed(range(root.rowCount())):
479 masterItem = root.child(i, self.COL_NAME)
480 if masterItem.master.name == master:
481 root.removeRow(i)
482 try:
483 del self.pyqt_workaround_sync[masterItem.master.name]
484 del self.pyqt_workaround_info[masterItem.master.name]
485 except:
486 pass
487 break
488
490 '''
491 Updates the errors reported by master_discovery.
492
493 @param master: the ROS master to update
494 @type master: C{str}
495 @param errors: the list with errors
496 @type errors: C{[str]}
497 '''
498 root = self.invisibleRootItem()
499 for i in reversed(range(root.rowCount())):
500 masterItem = root.child(i, self.COL_NAME)
501 if masterItem.master.name == master:
502 masterItem.updateMasterErrors(errors)
503 break
504
506 '''
507 Updates the time difference reported by master_discovery.
508
509 @param master: the ROS master to update
510 @type master: C{str}
511 @param timediff: the time difference to the host
512 @type timediff: float
513 '''
514 root = self.invisibleRootItem()
515 for i in reversed(range(root.rowCount())):
516 masterItem = root.child(i, self.COL_NAME)
517 if masterItem.master.name == master:
518 masterItem.updateTimeDiff(timediff)
519 break
520
522 '''
523 Updates the description of the master with given name.
524
525 @param master: the ROS master to add
526 @type master: C{str}
527 @param descr: the description of the master coded as HTML
528 @type descr: C{str}
529 '''
530 root = self.invisibleRootItem()
531 for i in range(root.rowCount()):
532 masterItem = root.child(i, self.COL_NAME)
533 if masterItem and masterItem.master.name == master:
534 masterItem.updateDescription(descr)
535
541