├── CHANGELOG.rst ├── CMakeLists.txt ├── LICENSE ├── bag_plugin.xml ├── package.xml ├── plugin.xml ├── resource ├── robotmonitor_mainwidget.ui └── timelinepane.ui ├── scripts └── rqt_robot_monitor ├── setup.py └── src └── rqt_robot_monitor ├── __init__.py ├── inspector_window.py ├── robot_monitor.py ├── robot_monitor_bag_plugin.py ├── robot_monitor_plugin.py ├── status_item.py ├── status_snapshot.py ├── timeline.py ├── timeline_pane.py ├── timeline_view.py └── util_robot_monitor.py /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package rqt_robot_monitor 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 0.5.15 (2023-10-02) 6 | ------------------- 7 | * Import setup from setuptools instead of distutils.core 8 | 9 | 0.5.14 (2022-03-16) 10 | ------------------- 11 | * Improve error and warning lists by only showing leaf-elements 12 | * Backported alternative view. 13 | 14 | 0.5.13 (2020-09-07) 15 | ------------------- 16 | * Fixed the functionality of the timeline_view 17 | 18 | 0.5.12 (2020-06-02) 19 | ------------------- 20 | * use catkin_install_python instead of install 21 | 22 | 0.5.11 (2020-05-13) 23 | ------------------- 24 | * add Python 3 conditional dependencies 25 | 26 | 0.5.10 (2020-04-19) 27 | ------------------- 28 | * updated package to format 3 29 | * added mutex and checks to fix (`#13 `_) 30 | 31 | 0.5.9 (2019-07-24) 32 | ------------------ 33 | * refactored code and made signaling more efficient 34 | * stale messages appear now in the error pane 35 | 36 | 0.5.8 (2017-09-26) 37 | ------------------ 38 | * Fix apparent threading bug in timeline_view (`#5 `_) 39 | 40 | 0.5.7 (2017-04-26) 41 | ------------------ 42 | 43 | 0.5.6 (2017-01-24) 44 | ------------------ 45 | * use Python 3 compatible syntax (`#121 `_) 46 | 47 | 0.5.5 (2016-11-02) 48 | ------------------ 49 | 50 | 0.5.4 (2016-09-19) 51 | ------------------ 52 | 53 | 0.5.3 (2016-05-16) 54 | ------------------ 55 | 56 | 0.5.2 (2016-04-29) 57 | ------------------ 58 | 59 | 0.5.1 (2016-04-28) 60 | ------------------ 61 | 62 | 0.5.0 (2016-04-27) 63 | ------------------ 64 | * Support Qt 5 (in Kinetic and higher) as well as Qt 4 (in Jade and earlier) (`#101 `_) 65 | 66 | 0.4.3 (2016-03-08) 67 | ------------------ 68 | 69 | 0.4.2 (2015-07-24) 70 | ------------------ 71 | 72 | 0.4.1 (2015-04-30) 73 | ------------------ 74 | * fix installing missing bag_plugin xml (`#288 `_) 75 | 76 | 0.4.0 (2014-11-05) 77 | ------------------ 78 | * rewrite rqt_robot_monitor (`#3 `_, `#9 `_, `#32 `_, `#33 `_, `#71 `_) 79 | * make diagnostics_agg a relative topic (`#32 `_) 80 | * add rqt_bag plugin 81 | * update script to use full plugin name 82 | 83 | 0.3.7 (2014-08-18) 84 | ------------------ 85 | 86 | 0.3.6 (2014-07-11) 87 | ------------------ 88 | 89 | 0.3.5 (2014-06-02) 90 | ------------------ 91 | 92 | 0.3.4 (2014-05-07) 93 | ------------------ 94 | 95 | 0.3.3 (2014-01-28) 96 | ------------------ 97 | 98 | 0.3.2 (2014-01-08) 99 | ------------------ 100 | * add groups for rqt plugins (`ros-visualization/rqt_common_plugins#167 `_) 101 | 102 | 0.3.1 (2013-10-09) 103 | ------------------ 104 | 105 | 0.3.0 (2013-08-28) 106 | ------------------ 107 | * fix indication when not receiving deagnostic data (`#35 `_) 108 | * fix scrolling of inspection window on new data (`#34 `_) 109 | 110 | 0.2.16 (2013-07-09) 111 | ------------------- 112 | * First public release into Hydro 113 | 114 | 0.2.15 (2013-04-25) 115 | ------------------- 116 | 117 | 0.2.14 (2013-04-12) 118 | ------------------- 119 | 120 | 0.2.13 (2013-04-09) 121 | ------------------- 122 | 123 | 0.2.12 (2013-04-06 18:22) 124 | ------------------------- 125 | 126 | 0.2.11 (2013-04-06 18:00) 127 | ------------------------- 128 | 129 | 0.2.10 (2013-04-04) 130 | ------------------- 131 | 132 | 0.2.9 (2013-03-07) 133 | ------------------ 134 | * Fix 135 | 136 | * now run with pyside (it used to be not working with it w/o having been noticed). 137 | * Call .ui file in .ui is now successfully working 138 | 139 | * Refactoring 140 | 141 | * Now TimelinePane uses .ui file 142 | 143 | 0.2.8 (2013-01-11) 144 | ------------------ 145 | 146 | 0.2.7 (2012-12-23 15:58) 147 | ------------------------ 148 | * first public release into Groovy 149 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(rqt_robot_monitor) 3 | # Load catkin and all dependencies required for this package 4 | find_package(catkin REQUIRED) 5 | catkin_package() 6 | catkin_python_setup() 7 | 8 | install(FILES 9 | bag_plugin.xml 10 | plugin.xml 11 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 12 | ) 13 | 14 | install(DIRECTORY resource 15 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 16 | ) 17 | 18 | catkin_install_python(PROGRAMS scripts/rqt_robot_monitor 19 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, rqt_robot_monitor 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 are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /bag_plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | An rqt_bag plugin for displaying diagnostics state 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | rqt_robot_monitor 7 | 0.5.15 8 | rqt_robot_monitor displays diagnostics_agg topics messages that 9 | are published by diagnostic_aggregator. 10 | rqt_robot_monitor is a direct port to rqt of 11 | robot_monitor. All 12 | diagnostics are fall into one of three tree panes depending on the status of 13 | diagnostics (normal, warning, error/stale). Status are shown in trees to 14 | represent their hierarchy. Worse status dominates the higher level status.
15 |
    16 | Ex. 'Computer' category has 3 sub devices. 2 are green but 1 is error. Then 17 | 'Computer' becomes error. 18 |
19 | You can look at the detail of each status by double-clicking the tree nodes.
20 | 21 | Currently re-usable API to other pkgs are not explicitly provided. 22 |
23 | Aaron Blasdel 24 | Arne Hitzmann 25 | Austin Hendrix 26 | 27 | BSD 28 | 29 | http://wiki.ros.org/rqt_robot_monitor 30 | https://github.com/ros-visualization/rqt_robot_monitor 31 | https://github.com/ros-visualization/rqt_robot_monitor/issues 32 | 33 | Austin Hendrix 34 | Isaac Saito 35 | Ze'ev Klapow 36 | Kevin Watts 37 | Josh Faust 38 | 39 | catkin 40 | python-setuptools 41 | python3-setuptools 42 | 43 | diagnostic_msgs 44 | python_qt_binding 45 | python-rospkg-modules 46 | python3-rospkg-modules 47 | qt_gui 48 | qt_gui_py_common 49 | rospy 50 | rqt_py_common 51 | rqt_gui 52 | rqt_gui_py 53 | rqt_bag 54 | 55 | 56 | 57 | 58 | 59 |
60 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Display for /diagnostics_agg 5 | 6 | 7 | 8 | 9 | folder 10 | Plugins related to robot tools. 11 | 12 | 13 | utilities-system-monitor 14 | A Python GUI plugin for monitoring the robot state using diagnostics messages. 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resource/robotmonitor_mainwidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RobotMonitorWidget 4 | 5 | 6 | 7 | 200 8 | 150 9 | 10 | 11 | 12 | Runtime Monitor 13 | 14 | 15 | 16 | 0 17 | 18 | 19 | 0 20 | 21 | 22 | 23 | 24 | Alternative view 25 | 26 | 27 | 28 | 29 | 30 | 31 | Qt::Vertical 32 | 33 | 34 | 9 35 | 36 | 37 | 38 | 39 | 75 40 | 25 41 | 42 | 43 | 44 | true 45 | 46 | 47 | true 48 | 49 | 50 | 51 | Error Device 52 | 53 | 54 | 55 | 56 | Message 57 | 58 | 59 | 60 | 61 | 62 | 63 | 75 64 | 25 65 | 66 | 67 | 68 | true 69 | 70 | 71 | true 72 | 73 | 74 | 75 | Warned Device 76 | 77 | 78 | 79 | 80 | Message 81 | 82 | 83 | 84 | 85 | 86 | 87 | 75 88 | 25 89 | 90 | 91 | 92 | true 93 | 94 | 95 | false 96 | 97 | 98 | true 99 | 100 | 101 | true 102 | 103 | 104 | 105 | All devices 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Message 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | RobotMonitorWidget 127 | QWidget 128 |
rqt_robot_monitor.robot_monitor
129 |
130 | 131 | TimelinePane 132 | QWidget 133 |
rqt_robot_monitor.timeline_pane
134 |
135 |
136 | 137 | 138 |
139 | -------------------------------------------------------------------------------- /resource/timelinepane.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TimelinePane 4 | 5 | 6 | 7 | 0 8 | 0 9 | 656 10 | 55 11 | 12 | 13 | 14 | 15 | 1 16 | 1 17 | 18 | 19 | 20 | 21 | 150 22 | 35 23 | 24 | 25 | 26 | 27 | 16777215 28 | 55 29 | 30 | 31 | 32 | Robot Monitor Timeline 33 | 34 | 35 | 36 | 0 37 | 38 | 39 | 0 40 | 41 | 42 | 43 | 44 | 0 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 9 53 | 54 | 55 | 56 | <-- old 57 | 58 | 59 | Qt::AutoText 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Qt::AlignCenter 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 9 78 | false 79 | 80 | 81 | 82 | new --> 83 | 84 | 85 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 16777215 96 | 20 97 | 98 | 99 | 100 | Qt::ScrollBarAlwaysOff 101 | 102 | 103 | Qt::ScrollBarAlwaysOff 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Qt::Vertical 115 | 116 | 117 | 118 | 20 119 | 40 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 50 129 | 25 130 | 131 | 132 | 133 | 134 | 80 135 | 50 136 | 137 | 138 | 139 | Pause 140 | 141 | 142 | true 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | TimelineView 153 | QGraphicsView 154 |
rqt_robot_monitor.timeline_view
155 |
156 |
157 | 158 | 159 |
160 | -------------------------------------------------------------------------------- /scripts/rqt_robot_monitor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | from rqt_gui.main import Main 6 | 7 | main = Main() 8 | sys.exit(main.main(sys.argv, standalone='rqt_robot_monitor.robot_monitor_plugin.RobotMonitorPlugin')) 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | from catkin_pkg.python_setup import generate_distutils_setup 5 | 6 | d = generate_distutils_setup( 7 | packages=['rqt_robot_monitor'], 8 | package_dir={'': 'src'} 9 | ) 10 | 11 | setup(**d) 12 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-visualization/rqt_robot_monitor/f9412e219d31b8d98a48b51d647dc70f9687d7c4/src/rqt_robot_monitor/__init__.py -------------------------------------------------------------------------------- /src/rqt_robot_monitor/inspector_window.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 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 Willow Garage, Inc. 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 | # Author: Isaac Saito, Ze'ev Klapow, Austin Hendrix 34 | 35 | from python_qt_binding.QtCore import Qt, Signal, Slot 36 | from python_qt_binding.QtWidgets import QPushButton, QTextEdit, QVBoxLayout, QWidget 37 | import rospy 38 | 39 | from rqt_robot_monitor.status_snapshot import StatusSnapshot, level_to_text 40 | from rqt_robot_monitor.timeline_pane import TimelinePane 41 | import rqt_robot_monitor.util_robot_monitor as util 42 | 43 | from diagnostic_msgs.msg import DiagnosticArray 44 | 45 | 46 | class InspectorWindow(QWidget): 47 | closed = Signal(str) 48 | _message_updated = Signal(dict) 49 | _queue_updated = Signal() 50 | 51 | def __init__(self, parent, name, timeline): 52 | """ 53 | :param name: Name of inspecting diagnostic status 54 | :param timeline: Timeline object from which a status is fetched 55 | """ 56 | #TODO(Isaac) UI construction that currently is done in this method, 57 | # needs to be done in .ui file. 58 | super(InspectorWindow, self).__init__(parent=parent) 59 | self.setWindowTitle(name) 60 | self._name = name 61 | 62 | self.layout_vertical = QVBoxLayout(self) 63 | 64 | self.disp = StatusSnapshot(parent=self) 65 | 66 | self.layout_vertical.addWidget(self.disp, 1) 67 | 68 | self._message_updated_processing = False 69 | self._queue_updated_processing = False 70 | 71 | self.timeline = timeline 72 | self.timeline.message_updated.connect( 73 | self.message_updated, Qt.DirectConnection) 74 | self.timeline.queue_updated.connect( 75 | self.queue_updated, Qt.DirectConnection) 76 | self._message_updated.connect( 77 | self._signal_message_updated, Qt.QueuedConnection) 78 | self._queue_updated.connect( 79 | self._signal_queue_updated, Qt.QueuedConnection) 80 | 81 | self.timeline_pane = TimelinePane(self, self.timeline.paused) 82 | self.timeline_pane.pause_changed.connect(self.timeline.set_paused) 83 | self.timeline_pane.position_changed.connect(self.timeline.set_position) 84 | self.timeline.pause_changed.connect(self.timeline_pane.set_paused) 85 | self.timeline.position_changed.connect(self.timeline_pane.set_position) 86 | self.layout_vertical.addWidget(self.timeline_pane, 0) 87 | 88 | self.snapshot = QPushButton("Snapshot") 89 | self.snapshot.clicked.connect(self._take_snapshot) 90 | self.layout_vertical.addWidget(self.snapshot) 91 | 92 | self.snaps = [] 93 | 94 | self.setLayout(self.layout_vertical) 95 | # TODO better to be configurable where to appear. 96 | self.resize(400, 600) 97 | 98 | def closeEvent(self, event): 99 | """ called when this window is closed 100 | 101 | Calls close on all snapshots, and emits the closed signal 102 | """ 103 | # TODO: are snapshots kept around even after they're closed? 104 | # this appears to work even if the user closes some snapshots, 105 | # and they're still left in the internal array 106 | for snap in self.snaps: 107 | snap.close() 108 | self.closed.emit(self._name) 109 | 110 | @Slot() 111 | def queue_updated(self): 112 | """ 113 | This method just calls _signal_queue_updated in 'best effort' manner. 114 | This method should be called by signal with DirectConnection. 115 | """ 116 | if self._queue_updated_processing: 117 | return 118 | self._queue_updated_processing = True 119 | self._queue_updated.emit() 120 | 121 | @Slot() 122 | def _signal_queue_updated(self): 123 | # update timeline pane 124 | # collect worst status levels for each history 125 | status = self.timeline.get_all_status_by_name(self._name) 126 | if status is not None: 127 | self.timeline_pane.set_levels([s.level for s in status]) 128 | self.timeline_pane.redraw.emit() 129 | self._queue_updated_processing = False 130 | 131 | @Slot(dict) 132 | def message_updated(self, status): 133 | """ 134 | This method just calls _signal_message_updated in 'best effort' manner. 135 | This method should be called by signal with DirectConnection. 136 | """ 137 | if self._message_updated_processing: 138 | return 139 | self._message_updated_processing = True 140 | self._message_updated.emit(status) 141 | 142 | @Slot(dict) 143 | def _signal_message_updated(self, status): 144 | rospy.logdebug('InspectorWin message_updated') 145 | 146 | try: 147 | status = status[self._name] 148 | except: 149 | return 150 | 151 | scroll_value = self.disp.verticalScrollBar().value() 152 | 153 | self.status = status 154 | self.disp.write_status.emit(status) 155 | 156 | if self.disp.verticalScrollBar().maximum() < scroll_value: 157 | scroll_value = self.disp.verticalScrollBar().maximum() 158 | self.disp.verticalScrollBar().setValue(scroll_value) 159 | self._message_updated_processing = False 160 | 161 | def _take_snapshot(self): 162 | snap = StatusSnapshot(status=self.status) 163 | self.snaps.append(snap) 164 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/robot_monitor.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 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 Willow Garage, Inc. 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 | # Author: Isaac Saito, Ze'ev Klapow, Austin Hendrix 34 | 35 | import os 36 | import rospkg 37 | 38 | from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus 39 | from python_qt_binding import loadUi 40 | from python_qt_binding.QtCore import QTimer, Signal, Qt, Slot 41 | from python_qt_binding.QtGui import QPalette 42 | from python_qt_binding.QtWidgets import QWidget, QTreeWidgetItem 43 | import rospy 44 | 45 | from rqt_robot_monitor.inspector_window import InspectorWindow 46 | from rqt_robot_monitor.status_item import StatusItem 47 | from rqt_robot_monitor.timeline_pane import TimelinePane 48 | from rqt_robot_monitor.timeline import Timeline 49 | import rqt_robot_monitor.util_robot_monitor as util 50 | 51 | 52 | class RobotMonitorWidget(QWidget): 53 | """ 54 | NOTE: RobotMonitorWidget.shutdown function needs to be called 55 | when the instance of this class terminates. 56 | 57 | RobotMonitorWidget itself doesn't store previous diagnostic states. 58 | It instead delegates that function to Timeline class. 59 | """ 60 | 61 | _TREE_ALL = 1 62 | _TREE_WARN = 2 63 | _TREE_ERR = 3 64 | 65 | _message_updated = Signal(dict) 66 | _queue_updated = Signal() 67 | 68 | def __init__(self, context, topic=None): 69 | """ 70 | :param context: plugin context hook to enable adding widgets as a 71 | ROS_GUI pane, 'PluginContext' 72 | :param topic: Diagnostic topic to subscribe to 'str' 73 | """ 74 | 75 | super(RobotMonitorWidget, self).__init__() 76 | rp = rospkg.RosPack() 77 | ui_file = os.path.join(rp.get_path('rqt_robot_monitor'), 'resource', 78 | 'robotmonitor_mainwidget.ui') 79 | loadUi(ui_file, self) 80 | 81 | obj_name = 'Robot Monitor' 82 | self.setObjectName(obj_name) 83 | self.setWindowTitle(obj_name) 84 | 85 | self._message_updated_processing = False 86 | self._queue_updated_processing = False 87 | 88 | # if we're given a topic, create a timeline. otherwise, don't 89 | # this can be used later when writing an rqt_bag plugin 90 | if topic: 91 | # create timeline data structure 92 | self._timeline = Timeline(topic, DiagnosticArray) 93 | self._timeline.message_updated.connect( 94 | self.message_updated, Qt.DirectConnection) 95 | self._timeline.queue_updated.connect( 96 | self.queue_updated, Qt.DirectConnection) 97 | self._message_updated.connect( 98 | self._signal_message_updated, Qt.QueuedConnection) 99 | self._queue_updated.connect( 100 | self._signal_queue_updated, Qt.QueuedConnection) 101 | 102 | # create timeline pane widget 103 | self._timeline_pane = TimelinePane(self, self._timeline.paused) 104 | self._timeline_pane.pause_changed.connect(self._timeline.set_paused) 105 | self._timeline_pane.position_changed.connect(self._timeline.set_position) 106 | self._timeline.pause_changed.connect(self._timeline_pane.set_paused) 107 | self._timeline.position_changed.connect(self._timeline_pane.set_position) 108 | 109 | self.vlayout_top.addWidget(self._timeline_pane) 110 | self._timeline_pane.show() 111 | else: 112 | self._timeline = None 113 | self._timeline_pane = None 114 | 115 | self._inspectors = {} 116 | 117 | self.tree_all_devices.itemDoubleClicked.connect(self._tree_clicked) 118 | self.warn_flattree.itemDoubleClicked.connect(self._tree_clicked) 119 | self.err_flattree.itemDoubleClicked.connect(self._tree_clicked) 120 | # TODO: resize on itemCollapsed and itemExpanded 121 | 122 | self._is_stale = False 123 | 124 | self._timer = QTimer() 125 | self._timer.timeout.connect(self._update_message_state) 126 | self._timer.start(1000) 127 | 128 | palette = self.tree_all_devices.palette() 129 | self._original_base_color = palette.base().color() 130 | self._original_alt_base_color = palette.alternateBase().color() 131 | 132 | self._tree = StatusItem(self.tree_all_devices.invisibleRootItem()) 133 | self._warn_tree = StatusItem(self.warn_flattree.invisibleRootItem()) 134 | self._err_tree = StatusItem(self.err_flattree.invisibleRootItem()) 135 | 136 | @Slot(dict) 137 | def message_updated(self, status): 138 | """ 139 | This method just calls _signal_message_updated in 'best effort' manner. 140 | This method should be called by signal with DirectConnection. 141 | """ 142 | if self._message_updated_processing: 143 | return 144 | self._message_updated_processing = True 145 | self._message_updated.emit(status) 146 | 147 | @Slot(dict) 148 | def _signal_message_updated(self, status): 149 | """ DiagnosticArray message callback """ 150 | 151 | if self.alternative_view_checkBox.isChecked(): 152 | # Walk the status array and update the tree 153 | for name, status in status.items(): 154 | # Compute path and walk to appropriate subtree 155 | path = name.split('/') 156 | tmp_tree = self._tree 157 | tmp_err_tree = self._err_tree 158 | tmp_warn_tree = self._warn_tree 159 | if path[0] == '': 160 | path = path[1:] 161 | tmp_tree.update(status, util.get_resource_name(name)) 162 | #tmp_tree = tmp_tree['Robot'] 163 | #tmp_tree.update(status, 'Robot') 164 | 165 | leaf = path[-1] 166 | for i, p in enumerate(path): 167 | if p != '': 168 | # Check for warnings 169 | if status.level is DiagnosticStatus.WARN: 170 | tmp_warn_tree[p].update(status, p) 171 | tmp_warn_tree = tmp_warn_tree[p] 172 | 173 | tmp_tree = tmp_tree[p] 174 | tmp_err_tree = tmp_err_tree[p] 175 | 176 | if p == leaf: 177 | tmp_tree.update(status, p) 178 | 179 | # Check for errors 180 | if status.level in [DiagnosticStatus.ERROR, DiagnosticStatus.STALE]: 181 | tmp_err_tree.update(status, p) 182 | self.err_flattree.expandAll() 183 | self.warn_flattree.expandAll() 184 | else: 185 | # Walk the status array and update the tree 186 | for name, status in status.items(): 187 | # Compute path and walk to appropriate subtree 188 | path = name.split('/') 189 | if path[0] == '': 190 | path = path[1:] 191 | tmp_tree = self._tree 192 | for p in path: 193 | tmp_tree = tmp_tree[p] 194 | tmp_tree.update(status, util.get_resource_name(name)) 195 | 196 | # Check for warnings, skip non-leaf elements 197 | if status.level == DiagnosticStatus.WARN and status.message != "Warning": 198 | self._warn_tree[name].update(status, name) 199 | 200 | # Check for errors, skip non-leaf elements 201 | if status.level in [DiagnosticStatus.ERROR, DiagnosticStatus.STALE] and status.message != "Error": 202 | self._err_tree[name].update(status, name) 203 | 204 | 205 | 206 | # For any items in the tree that were not updated, remove them 207 | self._tree.prune() 208 | self._warn_tree.prune() 209 | self._err_tree.prune() 210 | 211 | self.tree_all_devices.resizeColumnToContents(0) 212 | self.warn_flattree.resizeColumnToContents(0) 213 | self.err_flattree.resizeColumnToContents(0) 214 | 215 | self._message_updated_processing = False 216 | 217 | @Slot() 218 | def queue_updated(self): 219 | ''' 220 | This method just calls _signal_queue_updated in 'best effort' manner. 221 | This method should be called by signal with DirectConnection. 222 | ''' 223 | if self._queue_updated_processing: 224 | return 225 | self._queue_updated_processing = True 226 | self._queue_updated.emit() 227 | 228 | @Slot() 229 | def _signal_queue_updated(self): 230 | # update timeline pane 231 | # collect worst status levels for each history 232 | levels = [max([s.level for s in s.values()]) for s in self._timeline] 233 | self._timeline_pane.set_levels(levels) 234 | self._timeline_pane.redraw.emit() 235 | self._queue_updated_processing = False 236 | 237 | def resizeEvent(self, evt): 238 | """Overridden from QWidget""" 239 | rospy.logdebug('RobotMonitorWidget resizeEvent') 240 | if self._timeline_pane: 241 | self._timeline_pane.redraw.emit() 242 | 243 | @Slot(str) 244 | def _inspector_closed(self, name): 245 | """ Called when an inspector window is closed """ 246 | try: 247 | self._inspectors[name].deleteLater() 248 | del self._inspectors[name] 249 | except KeyError: 250 | pass 251 | 252 | @Slot(QTreeWidgetItem, int) 253 | def _tree_clicked(self, item, column): 254 | """ 255 | Slot to QTreeWidget.itemDoubleClicked 256 | 257 | :type item: QTreeWidgetItem 258 | :type column: int 259 | """ 260 | rospy.logdebug('RobotMonitorWidget _tree_clicked col=%d', column) 261 | 262 | if item.name in self._inspectors: 263 | self._inspectors[item.name].activateWindow() 264 | else: 265 | insp = InspectorWindow(None, item.name, self._timeline) 266 | insp.show() 267 | insp.closed.connect(self._inspector_closed) 268 | self._inspectors[item.name] = insp 269 | 270 | def _update_message_state(self): 271 | """ Update the display if it's stale """ 272 | if self._timeline is not None: 273 | if self._timeline.has_messages: 274 | previous_stale_state = self._is_stale 275 | self._is_stale = self._timeline.is_stale 276 | 277 | time_diff = int(self._timeline.data_age) 278 | 279 | msg_template = "Last message received %s %s ago" 280 | if time_diff == 1: 281 | msg = msg_template % (time_diff, "second") 282 | else: 283 | msg = msg_template % (time_diff, "seconds") 284 | self._timeline_pane._msg_label.setText(msg) 285 | if previous_stale_state != self._is_stale: 286 | self._update_background_color() 287 | else: 288 | # no messages received yet 289 | self._timeline_pane._msg_label.setText("No messages received") 290 | 291 | def _update_background_color(self): 292 | """ Update the background color based on staleness """ 293 | p = self.tree_all_devices.palette() 294 | if self._is_stale: 295 | p.setColor(QPalette.Base, Qt.darkGray) 296 | p.setColor(QPalette.AlternateBase, Qt.lightGray) 297 | else: 298 | p.setColor(QPalette.Base, self._original_base_color) 299 | p.setColor(QPalette.AlternateBase, self._original_alt_base_color) 300 | self.tree_all_devices.setPalette(p) 301 | self.warn_flattree.setPalette(p) 302 | self.err_flattree.setPalette(p) 303 | 304 | def shutdown(self): 305 | """ 306 | This needs to be called whenever this class terminates. 307 | This closes all the instances on all trees. 308 | Also unregisters ROS' subscriber, stops timer. 309 | """ 310 | rospy.logdebug('RobotMonitorWidget in shutdown') 311 | 312 | names = list(self._inspectors.keys()) 313 | for name in names: 314 | self._inspectors[name].close() 315 | 316 | if self._timeline: 317 | self._timeline.shutdown() 318 | 319 | self._timer.stop() 320 | del self._timer 321 | 322 | def save_settings(self, plugin_settings, instance_settings): 323 | instance_settings.set_value('splitter', self.splitter.saveState()) 324 | # TODO(ahendrix): persist the device paths, positions and sizes of any 325 | # inspector windows 326 | 327 | def restore_settings(self, plugin_settings, instance_settings): 328 | if instance_settings.contains('splitter'): 329 | self.splitter.restoreState(instance_settings.value('splitter')) 330 | else: 331 | self.splitter.setSizes([100, 100, 200]) 332 | # TODO(ahendrix): restore inspector windows 333 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/robot_monitor_bag_plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Software License Agreement (BSD License) 4 | # 5 | # Copyright (c) 2014, Austin Hendrix 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | # * Neither the name of Austin Hendrix. nor the names of its 19 | # contributors may be used to endorse or promote products derived 20 | # from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | # 35 | # Author: Austin Hendrix 36 | 37 | 38 | from rqt_bag.plugins.plugin import Plugin 39 | from rqt_bag import TopicMessageView 40 | 41 | from rqt_robot_monitor.robot_monitor import RobotMonitorWidget 42 | from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus 43 | 44 | class RobotMonitorBagPlugin(Plugin): 45 | def __init__(self): 46 | pass 47 | 48 | def get_view_class(self): 49 | return RobotMonitorBagView 50 | 51 | def get_renderer_class(self): 52 | return None 53 | 54 | def get_message_types(self): 55 | return ['diagnostic_msgs/DiagnosticArray'] 56 | 57 | class RobotMonitorBagView(TopicMessageView): 58 | name = 'Diagnostics Viewer' 59 | 60 | def __init__(self, timeline, parent, topic): 61 | super(RobotMonitorBagView, self).__init__(timeline, parent, topic) 62 | 63 | self._widget = RobotMonitorWidget(parent) 64 | parent.layout().addWidget(self._widget) 65 | 66 | def message_viewed(self, bag, msg_details): 67 | msg = msg_details[1] 68 | # generic conversion of DiagnosticStatus from bag type to current type 69 | # this should be fairly robust to minor changes in the message format 70 | status = [DiagnosticStatus(**dict((slot, getattr(m, slot)) for slot in m.__slots__)) for m in msg.status] 71 | msg = DiagnosticArray(msg.header, status) 72 | self._widget.message_updated.emit(msg) 73 | 74 | def close(self): 75 | self._widget.shutdown() # Closes unclosed popup windows. 76 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/robot_monitor_plugin.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 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 Willow Garage, Inc. 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 | # Author: Isaac Saito, Ze'ev Klapow, Austin Hendrix 34 | 35 | from qt_gui.plugin import Plugin 36 | 37 | from rqt_robot_monitor.robot_monitor import RobotMonitorWidget 38 | 39 | 40 | class RobotMonitorPlugin(Plugin): 41 | def __init__(self, context): 42 | """ 43 | :type context: qt_gui.PluginContext 44 | """ 45 | super(RobotMonitorPlugin, self).__init__(context) 46 | self._robot_monitor = RobotMonitorWidget(context, 'diagnostics_agg') 47 | if context.serial_number() > 1: 48 | self._robot_monitor.setWindowTitle( 49 | self._robot_monitor.windowTitle() + 50 | (' (%d)' % context.serial_number())) 51 | context.add_widget(self._robot_monitor) 52 | self.setObjectName('rqt Robot Monitor') 53 | 54 | def shutdown_plugin(self): 55 | """ 56 | Call RobotMonitorWidget's corresponding function. 57 | 58 | Overriding Plugin's method. 59 | """ 60 | self._robot_monitor.shutdown() # Closes unclosed popup windows. 61 | 62 | def save_settings(self, plugin_settings, instance_settings): 63 | """ 64 | Call RobotMonitorWidget's corresponding function. 65 | 66 | Overriding Plugin's method. 67 | """ 68 | self._robot_monitor.save_settings(plugin_settings, instance_settings) 69 | 70 | def restore_settings(self, plugin_settings, instance_settings): 71 | """ 72 | Call RobotMonitorWidget's corresponding function. 73 | 74 | Overriding Plugin's method. 75 | """ 76 | self._robot_monitor.restore_settings(plugin_settings, 77 | instance_settings) 78 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/status_item.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2014, Austin Hendrix 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 Austin Hendrix. 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 | # Author: Austin Hendrix 34 | 35 | from python_qt_binding.QtWidgets import QTreeWidgetItem 36 | import rqt_robot_monitor.util_robot_monitor as util 37 | 38 | class _StatusItem(QTreeWidgetItem): 39 | """ 40 | Internal subclass of QTreeWidgetItem which adds a 'name' member to make 41 | it easier to extract the item name and create an inspector when an item 42 | is clicked 43 | """ 44 | def __init__(self, name): 45 | super(_StatusItem, self).__init__() 46 | self.name = name 47 | 48 | class StatusItem(object): 49 | """ 50 | A class that wraps the default QTreeWidgetItem, so that we can manipulate 51 | all of the nodes in the tree in the same way (even the invisible root node) 52 | """ 53 | def __init__(self, item=None): 54 | self._children = {} 55 | self.updated = False 56 | if item is not None: 57 | self._item = item 58 | else: 59 | self._item = _StatusItem("NONAME") 60 | 61 | def update(self, status, displayname): 62 | self.updated = True 63 | self.displayname = displayname 64 | self._item.name = status.name 65 | self._item.setText(0, self.displayname) 66 | self._item.setIcon(0, util.level_to_icon(status.level)) 67 | self._item.setText(1, status.message) 68 | 69 | def prune(self): 70 | stale = [] 71 | for child in self._children: 72 | if not self._children[child].updated: 73 | stale.append(child) 74 | else: 75 | self._children[child].prune() 76 | if len(stale) > 0: 77 | for child in stale: 78 | self._item.removeChild(self._children[child]._item) 79 | del self._children[child] 80 | self.updated = False 81 | 82 | # functions that make this object behave like a dict 83 | def __getitem__(self, key): 84 | # if item doesn't exist, create a sane default for it 85 | if not key in self._children: 86 | self._children[key] = StatusItem() 87 | self._item.addChild(self._children[key]._item) 88 | return self._children[key] 89 | 90 | def __setitem__(self, key, value): 91 | # if item exists, remove it from the tree in addition to overwriting 92 | # it in the 'children' dict 93 | if key in self._children: 94 | self._item.removeChild(self._children[key]._item) 95 | self._children[key] = value 96 | self._item.addChild(value._item) 97 | 98 | def __contains__(self, key): 99 | return key in self._children 100 | 101 | def __iter__(self): 102 | for key in self._children: 103 | yield key 104 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/status_snapshot.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 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 Willow Garage, Inc. 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 | # Author: Isaac Saito, Ze'ev Klapow, Austin Hendrix 34 | # 35 | # TODO(ahendrix): 36 | # better formatting of key-value pairs in a table 37 | 38 | from python_qt_binding.QtWidgets import QTextEdit 39 | from python_qt_binding.QtCore import Signal 40 | 41 | from diagnostic_msgs.msg import DiagnosticStatus 42 | from rqt_robot_monitor.util_robot_monitor import level_to_text 43 | 44 | class StatusSnapshot(QTextEdit): 45 | """Display a single static status message. Helps facilitate copy/paste""" 46 | write_status = Signal(DiagnosticStatus) 47 | 48 | def __init__(self, status=None, parent=None): 49 | super(StatusSnapshot, self).__init__() 50 | 51 | self.write_status.connect(self._write_status) 52 | if status is not None: 53 | self.write_status.emit(status) 54 | 55 | self.resize(300, 400) 56 | self.show() 57 | 58 | def _write_status(self, status): 59 | self.clear() 60 | self._write("Full Name", status.name) 61 | self._write("Component", status.name.split('/')[-1]) 62 | self._write("Hardware ID", status.hardware_id) 63 | self._write("Level", level_to_text(status.level)) 64 | self._write("Message", status.message) 65 | self.insertPlainText('\n') 66 | 67 | for value in status.values: 68 | self._write(value.key, value.value) 69 | 70 | def _write(self, k, v): 71 | # TODO(ahendrix): write these as a table rather than as text 72 | self.setFontWeight(75) 73 | self.insertPlainText(str(k)) 74 | # TODO(ahendrix): de-dupe trailing ':' here 75 | self.insertPlainText(': ') 76 | 77 | self.setFontWeight(50) 78 | self.insertPlainText(str(v)) 79 | self.insertPlainText('\n') 80 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/timeline.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2014, Austin Hendrix 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 Austin Hendrix. 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 | # Author: Austin Hendrix 34 | 35 | from collections import deque 36 | from python_qt_binding.QtCore import Signal, Slot, QObject 37 | 38 | import rospy 39 | import threading 40 | 41 | from diagnostic_msgs.msg import DiagnosticArray 42 | 43 | 44 | class Timeline(QObject): 45 | """ 46 | A class which represents the status history of diagnostics 47 | It can be queried for a past history of diagnostics, and paused 48 | """ 49 | message_updated = Signal(dict) 50 | queue_updated = Signal() 51 | pause_changed = Signal(bool) 52 | position_changed = Signal(int) 53 | 54 | def __init__(self, topic, topic_type, count=30): 55 | super(Timeline, self).__init__() 56 | self._mutex = threading.RLock() 57 | self._queue = deque(maxlen=count) 58 | self._count = count 59 | self._current_index = -1 # rightmost item 60 | 61 | # the paused queue is a backup copy of the queue that is updated with 62 | # new messages while the timeline is paused, so that new messages and 63 | # new history are not lost 64 | self._paused_queue = None 65 | 66 | self._last_message_time = 0 67 | 68 | self._subscriber = rospy.Subscriber(topic, topic_type, self.callback, 69 | queue_size=10) 70 | 71 | def shutdown(self): 72 | """ 73 | Turn off this Timeline 74 | Internally, this just shuts down the subscriber 75 | """ 76 | self._subscriber.unregister() 77 | 78 | @Slot(bool) 79 | def set_paused(self, pause): 80 | """ 81 | Slot, to be called to change the pause status of the timeline 82 | 83 | This is generally intended to be connected to the status signal 84 | from a button or checkbox 85 | """ 86 | if pause != self.paused: 87 | with self._mutex: 88 | if pause: 89 | self._paused_queue = deque(self._queue, self._queue.maxlen) 90 | else: 91 | self._queue = self._paused_queue 92 | self._paused_queue = None 93 | 94 | # update pointer to latest message 95 | self._current_index = -1 96 | self.message_updated.emit(self._queue[self.position]) 97 | self.pause_changed.emit(pause) 98 | 99 | @property 100 | def paused(self): 101 | """ True if this timeline is paused """ 102 | with self._mutex: 103 | return self._paused_queue is not None 104 | 105 | def callback(self, msg): 106 | """ 107 | ROS Callback for new diagnostic messages 108 | 109 | Puts new msg into the queue, and emits a signal to let listeners know 110 | that the timeline has been updated 111 | 112 | If the timeline is paused, new messages are placed into a separate 113 | queue and swapped back in when the timeline is unpaused 114 | 115 | :type msg: Either DiagnosticArray or DiagnosticsStatus. Can be 116 | determined by __init__'s arg "msg_callback". 117 | """ 118 | self._last_message_time = rospy.get_time() 119 | dic = {status.name: status for status in msg.status} 120 | 121 | with self._mutex: 122 | if self.paused: 123 | self._paused_queue.append(dic) 124 | else: 125 | self._queue.append(dic) 126 | self.queue_updated.emit() 127 | if self.position == -1: 128 | self.message_updated.emit(dic) 129 | 130 | @property 131 | def has_messages(self): 132 | """ 133 | True if this timeline has received any messages. 134 | False if no messages have been received yet 135 | """ 136 | with self._mutex: 137 | return len(self._queue) > 0 138 | 139 | @property 140 | def data_age(self): 141 | """ Get the age (in seconds) of the most recent diagnostic message """ 142 | current_time = rospy.get_time() 143 | time_diff = current_time - self._last_message_time 144 | return time_diff 145 | 146 | @property 147 | def is_stale(self): 148 | """ True is this timeline is stale. """ 149 | return self.data_age > 10.0 150 | 151 | @property 152 | def position(self): 153 | index = self._current_index 154 | with self._mutex: 155 | while index < -1: 156 | index = len(self._queue) + index 157 | if index == len(self._queue) - 1: 158 | index = -1 159 | return index 160 | 161 | @position.setter 162 | def position(self, index): 163 | with self._mutex: 164 | max_index = len(self._queue) - 1 165 | min_index = -len(self._queue) 166 | index = min(index, max_index) 167 | index = max(index, min_index) 168 | if index != self._current_index or self._current_index == -1: 169 | self._current_index = index 170 | self.message_updated.emit(self._queue[index]) 171 | 172 | @Slot(int) 173 | def set_position(self, position): 174 | self.position = position 175 | self.position_changed.emit(position) 176 | 177 | def get_current_status_by_name(self, name): 178 | with self._mutex: 179 | return self._queue[self.position][name] 180 | 181 | def get_all_status_by_name(self, name): 182 | with self._mutex: 183 | try: 184 | return [status[name] for status in list(self._queue)] 185 | except: 186 | return None 187 | 188 | def __len__(self): 189 | with self._mutex: 190 | return len(self._queue) 191 | 192 | def __iter__(self): 193 | with self._mutex: 194 | for msg in list(self._queue): 195 | yield msg 196 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/timeline_pane.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 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 Willow Garage, Inc. 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 | # Author: Isaac Saito, Ze'ev Klapow, Austin Hendrix 34 | 35 | import os 36 | 37 | from python_qt_binding import loadUi 38 | from python_qt_binding.QtCore import Signal, Slot 39 | from python_qt_binding.QtWidgets import QWidget 40 | import rospy 41 | import rospkg 42 | 43 | from rqt_robot_monitor.timeline import Timeline 44 | 45 | 46 | class TimelinePane(QWidget): 47 | """ 48 | This class defines the pane where timeline and its related components 49 | are displayed. 50 | """ 51 | status_updated = Signal(list) 52 | pause_changed = Signal(bool) 53 | position_changed = Signal(int) 54 | redraw = Signal() 55 | 56 | def __init__(self, parent, paused=False): 57 | """ 58 | Because this class is intended to be instantiated via Qt's .ui file, 59 | taking argument other than parent widget is not possible, which is 60 | ported to set_timeline_data method. That said, set_timeline_data must 61 | be called (soon) after an object of this is instantiated. 62 | """ 63 | super(TimelinePane, self).__init__(parent=parent) 64 | 65 | rp = rospkg.RosPack() 66 | ui_file = os.path.join(rp.get_path('rqt_robot_monitor'), 67 | 'resource', 68 | 'timelinepane.ui') 69 | loadUi(ui_file, self) 70 | 71 | self._timeline_view.show() 72 | self._timeline_view.position_changed.connect(self._signal_position_changed) 73 | 74 | # connect pause button 75 | self._pause_button.clicked[bool].connect(self._signal_pause_changed) 76 | 77 | # bootstrap initial state 78 | self._pause_button.setChecked(paused) 79 | 80 | self.redraw.connect(self._signal_redraw) 81 | 82 | def _signal_pause_changed(self, paused): 83 | self.pause_changed.emit(paused) 84 | 85 | def _signal_position_changed(self, position): 86 | self.position_changed.emit(position) 87 | 88 | @Slot(bool) 89 | def set_paused(self, paused): 90 | self._pause_button.setChecked(paused) 91 | 92 | @Slot(list) 93 | def set_levels(self, levels): 94 | """ 95 | :param levels: List of status levels 96 | """ 97 | self._timeline_view.set_levels(levels) 98 | 99 | @Slot(int) 100 | def set_position(self, position): 101 | self._timeline_view.set_marker_pos(position) 102 | 103 | @Slot() 104 | def _signal_redraw(self): 105 | self._timeline_view.redraw.emit() 106 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/timeline_view.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 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 Willow Garage, Inc. 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 | # Author: Isaac Saito, Ze'ev Klapow, Austin Hendrix 34 | 35 | from math import floor 36 | from collections import deque 37 | import rospy 38 | 39 | from python_qt_binding.QtCore import QPointF, Signal, Slot 40 | from python_qt_binding.QtGui import QColor, QIcon 41 | from python_qt_binding.QtWidgets import QGraphicsPixmapItem, QGraphicsView, \ 42 | QGraphicsScene 43 | 44 | import rqt_robot_monitor.util_robot_monitor as util 45 | from diagnostic_msgs.msg import DiagnosticStatus 46 | 47 | 48 | class TimelineView(QGraphicsView): 49 | """ 50 | This class draws a graphical representation of a timeline. 51 | 52 | This is ONLY the bar and colored boxes. 53 | 54 | When you instantiate this class, do NOT forget to call set_init_data to 55 | set necessary data. 56 | """ 57 | 58 | paused = Signal(bool) 59 | position_changed = Signal(int) 60 | redraw = Signal() 61 | 62 | def __init__(self, parent=None): 63 | """Cannot take args other than parent due to loadUi limitation.""" 64 | 65 | super(TimelineView, self).__init__(parent=parent) 66 | self._timeline_marker = QIcon.fromTheme('system-search') 67 | 68 | self._min = 0 69 | self._max = 0 70 | self._xpos_marker = -1 71 | 72 | self._timeline_marker_width = 15 73 | self._timeline_marker_height = 15 74 | self._last_marker_at = -1 75 | 76 | self.setUpdatesEnabled(True) 77 | self._scene = QGraphicsScene(self) 78 | self.setScene(self._scene) 79 | 80 | self._levels = None 81 | 82 | self.redraw.connect(self._signal_redraw) 83 | 84 | def mouseReleaseEvent(self, event): 85 | """ 86 | :type event: QMouseEvent 87 | """ 88 | xpos = self.pos_from_x(event.x()) 89 | self.set_marker_pos(xpos) 90 | 91 | def mousePressEvent(self, event): 92 | """ 93 | :type event: QMouseEvent 94 | """ 95 | # Pause the timeline 96 | self.paused.emit(True) 97 | 98 | xpos = self.pos_from_x(event.x()) 99 | self.set_marker_pos(xpos) 100 | 101 | def mouseMoveEvent(self, event): 102 | """ 103 | :type event: QMouseEvent 104 | """ 105 | xpos = self.pos_from_x(event.x()) 106 | self.set_marker_pos(xpos) 107 | 108 | def pos_from_x(self, x): 109 | """ 110 | Get the index in the timeline from the mouse click position 111 | 112 | :param x: Position relative to self widget. 113 | :return: Index 114 | """ 115 | width = self.size().width() 116 | # determine value from mouse click 117 | width_cell = width / float(max(len(self._levels), 1)) 118 | position = int(floor(x / width_cell)) 119 | if position == len(self._levels) - 1: 120 | return -1 121 | return position 122 | 123 | @Slot(int) 124 | def set_marker_pos(self, xpos): 125 | """ 126 | Set marker position from index 127 | 128 | :param xpos: Marker index 129 | """ 130 | if self._levels is None: 131 | rospy.logwarn('Called set_marker_pos before set_levels') 132 | return 133 | 134 | if xpos == -1: 135 | # stick to the latest when position is -1 136 | self._xpos_marker = -1 137 | # check if we chose latest item 138 | if self._last_marker_at != self._xpos_marker: 139 | # update variable to check for change during next round 140 | self._last_marker_at = self._xpos_marker 141 | # emit change to all timeline_panes 142 | self.position_changed.emit(self._xpos_marker) 143 | self.redraw.emit() 144 | return 145 | 146 | self._xpos_marker = self._clamp(xpos, self._min, self._max) 147 | 148 | if self._xpos_marker == self._last_marker_at: 149 | # Clicked the same pos as last time. 150 | return 151 | elif self._xpos_marker >= len(self._levels): 152 | # When clicked out-of-region 153 | return 154 | 155 | self._last_marker_at = self._xpos_marker 156 | 157 | # Set timeline position. This broadcasts the message at that position 158 | # to all of the other viewers 159 | self.position_changed.emit(self._xpos_marker) 160 | self.redraw.emit() 161 | 162 | def _clamp(self, val, min, max): 163 | """ 164 | Judge if val is within the range given by min & max. 165 | If not, return either min or max. 166 | 167 | :type val: any number format 168 | :type min: any number format 169 | :type max: any number format 170 | :rtype: int 171 | """ 172 | if (val < min): 173 | return min 174 | if (val > max): 175 | return max 176 | return val 177 | 178 | @Slot(list) 179 | def set_levels(self, levels): 180 | self._levels = levels 181 | self.redraw.emit() 182 | 183 | @Slot() 184 | def _signal_redraw(self): 185 | """ 186 | Gets called either when new msg comes in or when marker is moved by 187 | user. 188 | """ 189 | if self._levels is None: 190 | return 191 | 192 | # update the limits 193 | self._min = 0 194 | self._max = len(self._levels) - 1 195 | 196 | self._scene.clear() 197 | 198 | qsize = self.size() 199 | width_tl = qsize.width() 200 | 201 | w = width_tl / float(max(len(self._levels), 1)) 202 | is_enabled = self.isEnabled() 203 | 204 | for i, level in enumerate(self._levels): 205 | h = self.viewport().height() 206 | 207 | # Figure out each cell's color. 208 | qcolor = QColor('grey') 209 | if is_enabled and level is not None: 210 | if level > DiagnosticStatus.ERROR: 211 | # Stale items should be reported as errors unless all stale 212 | level = DiagnosticStatus.ERROR 213 | qcolor = util.level_to_color(level) 214 | # TODO Use this code for adding gradation to the cell color. 215 | # end_color = QColor(0.5 * QColor('red').value(), 216 | # 0.5 * QColor('green').value(), 217 | # 0.5 * QColor('blue').value()) 218 | 219 | self._scene.addRect(w * i, 0, w, h, QColor('white'), qcolor) 220 | 221 | # Getting marker index. 222 | xpos_marker = self._xpos_marker 223 | 224 | # If marker is -1 for latest use (number_of_cells -1) 225 | if xpos_marker == -1: 226 | xpos_marker = len(self._levels) - 1 227 | 228 | # Convert get horizontal pixel value of selected cell's center 229 | xpos_marker_in_pixel = (xpos_marker * w + 230 | (w / 2.0) - (self._timeline_marker_width / 2.0)) 231 | pos_marker = QPointF(xpos_marker_in_pixel, 0) 232 | 233 | # Need to instantiate marker everytime since it gets deleted 234 | # in every loop by scene.clear() 235 | timeline_marker = self._instantiate_tl_icon() 236 | timeline_marker.setPos(pos_marker) 237 | self._scene.addItem(timeline_marker) 238 | 239 | def _instantiate_tl_icon(self): 240 | timeline_marker_icon = QIcon.fromTheme('system-search') 241 | timeline_marker_icon_pixmap = timeline_marker_icon.pixmap( 242 | self._timeline_marker_width, 243 | self._timeline_marker_height) 244 | return QGraphicsPixmapItem(timeline_marker_icon_pixmap) 245 | -------------------------------------------------------------------------------- /src/rqt_robot_monitor/util_robot_monitor.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2012, Willow Garage, Inc. 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 Willow Garage, Inc. 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 | # Author: Isaac Saito, Ze'ev Klapow, Austin Hendrix 34 | 35 | from diagnostic_msgs.msg import DiagnosticStatus 36 | from python_qt_binding.QtGui import QColor, QIcon 37 | import rospy 38 | 39 | # TODO: Utils and common configs are mixed in this class. 40 | 41 | # Instantiating icons that show the device status. 42 | _ERR_ICON = QIcon.fromTheme('dialog-error') 43 | _WARN_ICON = QIcon.fromTheme('dialog-warning') 44 | _OK_ICON = QIcon.fromTheme('emblem-default') 45 | # Added following this QA thread http://goo.gl/83tVZ 46 | _STALE_ICON = QIcon.fromTheme('dialog-question') 47 | 48 | _LEVEL_TO_ICON = {0: _OK_ICON, 1: _WARN_ICON, 2: _ERR_ICON, 3: _STALE_ICON} 49 | 50 | _LEVEL_TO_COLOR = {0: QColor(85, 178, 76), # green 51 | 1: QColor(222, 213, 17), # yellow 52 | 2: QColor(178, 23, 46), # red 53 | 3: QColor(40, 23, 176) # blue 54 | } 55 | 56 | _LEVEL_TO_TEXT = { 0: "OK", 1: "WARNING", 2: "ERROR", 3: "STALE" } 57 | 58 | def level_to_icon(level): 59 | if level in _LEVEL_TO_ICON: 60 | return _LEVEL_TO_ICON[level] 61 | else: 62 | return _ERR_ICON 63 | 64 | def level_to_color(level): 65 | if level in _LEVEL_TO_COLOR: 66 | return _LEVEL_TO_COLOR[level] 67 | else: 68 | return _LEVEL_TO_COLOR[2] 69 | 70 | def level_to_text(level): 71 | if level in _LEVEL_TO_TEXT: 72 | return _LEVEL_TO_TEXT[level] 73 | else: 74 | return "UNKNOWN(%d)" % ( level ) 75 | 76 | def get_resource_name(status_name): 77 | """ 78 | Get resource name from path 79 | 80 | :param: status_name is a string that may consists of status names that 81 | are delimited by slash. 82 | :rtype: str 83 | """ 84 | name = status_name.split('/')[-1] 85 | rospy.logdebug(' get_resource_name name = %s', name) 86 | return name 87 | 88 | def get_color_for_message(msg): 89 | """ 90 | Get the overall (worst) color for a DiagnosticArray 91 | :param msg: DiagnosticArray 92 | """ 93 | level = 0 94 | min_level = 255 95 | 96 | lookup = {} 97 | for status in msg.status: 98 | if (status.level > level): 99 | level = status.level 100 | if (status.level < min_level): 101 | min_level = status.level 102 | 103 | # Stale items should be reported as errors unless all stale 104 | if (level > 2 and min_level <= 2): 105 | level = 2 106 | 107 | rospy.logdebug(' get_color_for_message color lv=%d', level) 108 | return level_to_color(level) 109 | 110 | def get_status_by_name(msg, name): 111 | for status in msg.status: 112 | if status.name == name: 113 | return status 114 | return None 115 | --------------------------------------------------------------------------------