Package node_manager_fkie :: Module master_list_model
[frames] | no frames]

Source Code for Module node_manager_fkie.master_list_model

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2012, Fraunhofer FKIE/US, Alexander Tiderko 
  4  # All rights reserved. 
  5  # 
  6  # Redistribution and use in source and binary forms, with or without 
  7  # modification, are permitted provided that the following conditions 
  8  # are met: 
  9  # 
 10  #  * Redistributions of source code must retain the above copyright 
 11  #    notice, this list of conditions and the following disclaimer. 
 12  #  * Redistributions in binary form must reproduce the above 
 13  #    copyright notice, this list of conditions and the following 
 14  #    disclaimer in the documentation and/or other materials provided 
 15  #    with the distribution. 
 16  #  * Neither the name of Fraunhofer nor the names of its 
 17  #    contributors may be used to endorse or promote products derived 
 18  #    from this software without specific prior written permission. 
 19  # 
 20  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 21  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 22  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 23  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 24  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 25  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 26  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 27  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 28  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 29  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 30  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 31  # POSSIBILITY OF SUCH DAMAGE. 
 32   
 33  from python_qt_binding.QtCore import 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 
44 45 46 -class MasterSyncButtonHelper(QObject):
47 ''' 48 This is helper class to which contains a button and can emit signals. The 49 MasterSyncItem can not emit signals, but is used in QStandardModel. 50 ''' 51 clicked = Signal(bool, str) 52 53 NOT_SYNC = 0 54 SWITCHED = 1 55 SYNC = 2 56 57 ICON_PREFIX = 'irondevil' 58 # ICON_PREFIX = 'crystal_clear' 59
60 - def __init__(self, master):
61 QObject.__init__(self) 62 self.name = master.name 63 self._master = master 64 self._syncronized = MasterSyncButtonHelper.NOT_SYNC 65 self.ICONS = {MasterSyncButtonHelper.SYNC: QIcon(":/icons/%s_sync.png" % self.ICON_PREFIX), 66 MasterSyncButtonHelper.NOT_SYNC: QIcon(":/icons/%s_not_sync.png" % self.ICON_PREFIX), 67 MasterSyncButtonHelper.SWITCHED: QIcon(":/icons/%s_start_sync.png" % self.ICON_PREFIX)} 68 self.widget = QPushButton() 69 # self.widget.setFlat(True) 70 self.widget.setIcon(self.ICONS[MasterSyncButtonHelper.NOT_SYNC]) 71 self.widget.setMaximumSize(48, 48) 72 self.widget.setCheckable(True) 73 self.widget.clicked.connect(self.on_sync_clicked)
74
75 - def on_sync_clicked(self, checked):
76 self.set_sync_state(MasterSyncButtonHelper.SWITCHED) 77 self.clicked.emit(checked, self._master.uri)
78
79 - def master(self):
80 return self._master
81
82 - def get_sync_state(self):
83 return self._syncronized
84
85 - def set_sync_state(self, value):
86 if self._syncronized != value: 87 self._syncronized = value 88 if value in self.ICONS: 89 self.widget.setIcon(self.ICONS[value]) 90 self.widget.setChecked(value == MasterSyncButtonHelper.SYNC)
91
92 - def __eq__(self, item):
93 if isinstance(item, str) or isinstance(item, unicode): 94 return self.master.name.lower() == item.lower() 95 elif not (item is None): 96 return self.master.name.lower() == item.master.name.lower() 97 return False
98
99 - def __gt__(self, item):
100 if isinstance(item, str) or isinstance(item, unicode): 101 return self.master.name.lower() > item.lower() 102 elif not (item is None): 103 return self.master.name.lower() > item.master.name.lower() 104 return False
105
106 107 -class MasterSyncItem(QStandardItem):
108 ''' 109 This object is needed to insert into the QStandardModel. 110 ''' 111 ITEM_TYPE = QStandardItem.UserType + 35 112
113 - def __init__(self, master):
114 QStandardItem.__init__(self) 115 self.name = master.name 116 self.button = MasterSyncButtonHelper(master) 117 self.parent_item = None
118 119 @property
120 - def master(self):
121 return self.button.master()
122 123 @property
124 - def synchronized(self):
125 return self.button.get_sync_state()
126 127 @synchronized.setter
128 - def synchronized(self, value):
129 self.button.set_sync_state(value)
130
131 - def __eq__(self, item):
132 return self.button == item
133
134 - def __gt__(self, item):
135 return self.button > item
136
137 138 -class MasterItem(QStandardItem):
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
167 - def _threaded_get_ip(self):
168 thread = threading.Thread(target=self.__get_ip) 169 thread.daemon = True 170 thread.start()
171
172 - def __get_ip(self):
173 try: 174 # get the IP of the master uri 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 # self.updateNameView(self.master, self.quality, self) 188 except: 189 import traceback 190 print traceback.format_exc(1)
191 192 @property
193 - def master(self):
194 return self._master
195 196 @master.setter
197 - def master(self, value):
198 self._master = value
199 200 @property
201 - def quality(self):
202 return self.__quality
203 204 @quality.setter
205 - def quality(self, value):
206 if self.__quality != value: 207 self.__quality = value 208 self.updateMasterView(self.parent_item)
209
210 - def updateMasterErrors(self, error_list):
211 self._master_errors = error_list 212 self.updateNameView(self.master, self.quality, self)
213
214 - def updateTimeDiff(self, timediff):
215 self._timediff = timediff 216 self.updateNameView(self.master, self.quality, self)
217
218 - def updateMasterView(self, parent):
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 # update the name decoration 229 child = parent.child(self.row(), MasterModel.COL_NAME) 230 if child is not None: 231 self.updateNameView(self.master, self.quality, child)
232
233 - def updateNameView(self, master, quality, item):
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 # update the icon 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
284 - def updateDescription(self, descr):
285 self.descr = descr 286 self.updateNameView(self.master, self.quality, self)
287 288 @classmethod
289 - def toHTML(cls, text):
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
304 - def type(self):
305 return MasterItem.ITEM_TYPE
306
307 - def __eq__(self, item):
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
314 - def __gt__(self, item):
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: # local hosts are at the top 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: # local hosts are at the top 326 return False 327 return self.master.name.lower() > item.master.name.lower() 328 return False
329
330 331 -class MasterModel(QStandardItemModel):
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() # workaround for using with PyQt: store the python object to keep the defined attributes in the MasterItem subclass
353
354 - def flags(self, index):
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 # item = self.itemFromIndex(index) 365 # if item and item.master.online: 366 return Qt.ItemIsSelectable | Qt.ItemIsEnabled
367 # return Qt.NoItemFlags 368
369 - def updateMaster(self, master):
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 # remove master, if his name was changed but not the ROS master URI 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 # update or add a the item 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 # update item 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 # workaround for using with PyQt: store the python object to keep the defined attributes in the MasterItem subclass 426 # add the items to the data model 427 if index > -1: 428 root.insertRow(index, items) 429 else: 430 root.appendRow(items) 431 # add the sync botton and connect the signals 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
438 - def updateMasterStat(self, master, quality):
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
454 - def setChecked(self, master, state):
455 ''' 456 Set the master to checked state 457 458 @param master: the ROS master to update 459 @type master: C{str} 460 @param state: new state 461 @type state: C{bool} 462 ''' 463 root = self.invisibleRootItem() 464 for i in reversed(range(root.rowCount())): 465 masterItem = root.child(i, self.COL_SYNC) 466 if masterItem.master.name == master: 467 masterItem.synchronized = MasterSyncButtonHelper.SYNC if state else MasterSyncButtonHelper.NOT_SYNC 468 break
469
470 - def removeMaster(self, master):
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
489 - def updateMasterErrors(self, master, errors):
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
505 - def updateTimeDiff(self, master, timediff):
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
521 - def updateDescription(self, master, descr):
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
536 - def on_sync_clicked(self, checked, masteruri):
537 if checked: 538 self.sync_start.emit(masteruri) 539 else: 540 self.sync_stop.emit(masteruri)
541