|
| 1 | +--- |
| 2 | +# Jekyll 'Front Matter' goes here. Most are set by default, and should NOT be |
| 3 | +# overwritten except in special circumstances. |
| 4 | +# You should set the date the article was last updated like this: |
| 5 | +date: 2023-05-02 # YYYY-MM-DD |
| 6 | +# This will be displayed at the bottom of the article |
| 7 | +# You should set the article's title: |
| 8 | +title: Framework GUI for robotic system using ROS |
| 9 | +# The 'title' is automatically displayed at the top of the page |
| 10 | +# and used in other parts of the site. |
| 11 | +--- |
| 12 | +PyQt is an easy to implement user interface that can be integrated with ROS. It is customizable and can be deployed on multiple platforms. This documentation provides a basic GUI framework that all robotic systems using ROS can use and build upon. |
| 13 | + |
| 14 | +## Requirements |
| 15 | + |
| 16 | +- This application assumes your workspace has ROS installed. If not, use [this link](http://wiki.ros.org/ROS/Installation) for installing ROS. |
| 17 | + |
| 18 | +- This application can be used on any Operating System including Linux, Windows and Mac. |
| 19 | + |
| 20 | +- This application can be used with ROS1 and ROS2. |
| 21 | + |
| 22 | + |
| 23 | +### Installation |
| 24 | + |
| 25 | +To use PyQt5, open a terminal and type the following command |
| 26 | + |
| 27 | +```bash |
| 28 | +$ pip install PyQt5 |
| 29 | +``` |
| 30 | + |
| 31 | +## Overview of Final Application |
| 32 | + |
| 33 | +This is the final application you can get by following this tutorial. Blocks that require customization will be explained in the documentation. |
| 34 | + |
| 35 | + |
| 36 | + |
| 37 | +## Application Walkthrough |
| 38 | + |
| 39 | +### Timers |
| 40 | + |
| 41 | +The application has two timers |
| 42 | + |
| 43 | +1. System level timer: allows you to keep track of your entire system's operation time. |
| 44 | + |
| 45 | +2. Task level timer: allows you to keep track of a specific task's operation time. |
| 46 | + |
| 47 | +### System Level Timer |
| 48 | + |
| 49 | +The system level timer captures the time taken between the start and the end of your system. This can be done by pressing the 'Start System Timer' button. Clicking it once begins the timer and clicking it again stops the timer. The color of the timer will change based on the ` color_change_times ` and ` color_change_colors` defined in the full code at the bottom of this tutorial. This can be modified to suit your system's requirements. |
| 50 | + |
| 51 | +The color change for the timers are as follows: |
| 52 | + |
| 53 | +Green Button |
| 54 | + |
| 55 | +Yellow Button |
| 56 | + |
| 57 | +Orange Button |
| 58 | + |
| 59 | +Red Button |
| 60 | + |
| 61 | + |
| 62 | +The code block for this is given here: |
| 63 | +``` |
| 64 | +def SystemTimerBlock(self): |
| 65 | + self.system_count = 0 |
| 66 | + self.system_start = False |
| 67 | + self.system_end = False |
| 68 | +
|
| 69 | + self.system_timer_button = QPushButton("Start System Timer", self) |
| 70 | + self.system_timer_button.setFixedSize(self.win.frameGeometry().width(),self.win.frameGeometry().height()//4) |
| 71 | + self.grid.addWidget(self.system_timer_button, 0, 2) |
| 72 | + self.system_timer_button.clicked.connect(self.system_timer_controller) |
| 73 | +
|
| 74 | + system_timer = QTimer(self) |
| 75 | + system_timer.timeout.connect(self.system_timer) |
| 76 | + system_timer.start(1000) # modify to match system needs |
| 77 | +``` |
| 78 | + |
| 79 | +### Task Level Timer |
| 80 | + |
| 81 | +The task level timer captures the time taken between the start and the end of one task. This can be done by pressing the 'Start Task Timer' button. This button also changes color when the time taken to perform a task is reaching the total allowed time. |
| 82 | + |
| 83 | +One additional feature of the task level timer is it saves the task logs. These are outputted in the `Task Times` block that is directly below the button. |
| 84 | + |
| 85 | + |
| 86 | + |
| 87 | +``` |
| 88 | +def TaskTimerBlock(self): |
| 89 | + self.task_times = {} |
| 90 | + self.task_count = 0 |
| 91 | + self.task_start = False |
| 92 | + self.task_end = False |
| 93 | +
|
| 94 | + self.task_timer_button = QPushButton("Start Task Timer", self) |
| 95 | + self.task_timer_button.setFixedSize(self.win.frameGeometry().width(),self.win.frameGeometry().height()//4) |
| 96 | + self.grid.addWidget(self.task_timer_button, 1, 2) |
| 97 | + self.task_timer_button.clicked.connect(self.task_timer_controller) |
| 98 | +
|
| 99 | + task_timer = QTimer(self) |
| 100 | + task_timer.timeout.connect(self.task_timer) |
| 101 | + task_timer.start(1000) # modify to match system needs |
| 102 | +
|
| 103 | + self.task_times_label = QLabel("Task Times", self) |
| 104 | + self.grid.addWidget(self.task_times_label, 2, 2, 2, 1) |
| 105 | + self.task_times_label.setStyleSheet("border : 3px solid black") |
| 106 | + self.task_times_label.setFont(QFont('Times', 10)) |
| 107 | + self.task_times_label.setAlignment(Qt.AlignCenter) |
| 108 | +``` |
| 109 | + |
| 110 | +### E-Stop Button |
| 111 | + |
| 112 | +The E-Stop button is a ROS publisher that will publish to a topic which can then be used by your system's code to stop the robot. |
| 113 | + |
| 114 | +You need to change the following code |
| 115 | +``` |
| 116 | +self.estop_pub = rospy.Publisher('/mrsd/estop', Bool, queue_size=10) |
| 117 | +``` |
| 118 | +and customize it for your system. Your ROS system's main code should have an e-stop subscriber that will shut down the entire system once the button is pressed. |
| 119 | + |
| 120 | + |
| 121 | +### System Status |
| 122 | + |
| 123 | +The system status block subscribes to the following topic. |
| 124 | +``` |
| 125 | +self.status_sub = rospy.Subscriber('/mrsd/status', String, self.status_callback) |
| 126 | +``` |
| 127 | +Thus your main system should publish to a `/mrsd/status` topic. Ideally, your state machine will have a topic publisher that this application can subscribe to. |
| 128 | + |
| 129 | +### System Log |
| 130 | + |
| 131 | +This block is left for you to customize and display any other useful information. It subscribes to |
| 132 | + |
| 133 | +``` |
| 134 | +self.system_log_sub = rospy.Subscriber('/mrsd/system_log', String, self.system_log_callback) |
| 135 | +``` |
| 136 | + |
| 137 | +and you need to add some code to format the output. For example, this section could be used to display some of these information |
| 138 | + |
| 139 | +1. How many peppers you have harvested |
| 140 | +2. How many people you have saved |
| 141 | +3. What process are up next |
| 142 | + |
| 143 | + |
| 144 | +### Visualization Block |
| 145 | + |
| 146 | +The visualization block can be used to display any results visually. Use cases can be |
| 147 | + |
| 148 | +1. Robot localization within the map |
| 149 | +2. 3D point clouds |
| 150 | +3. Object detection results |
| 151 | + |
| 152 | +### Entire code |
| 153 | +<details> |
| 154 | + <summary>pyqt-ros.py</summary> |
| 155 | + |
| 156 | + ``` |
| 157 | + # importing libraries |
| 158 | +from PyQt5.QtWidgets import * |
| 159 | +from PyQt5.QtGui import * |
| 160 | +from PyQt5.QtCore import * |
| 161 | +import sys, emoji, rospy |
| 162 | +from PyQt5.QtGui import QPixmap |
| 163 | +
|
| 164 | +# system level requirements |
| 165 | +total_demo_time = 60*20 # assuming SVD is 20 minutes |
| 166 | +one_task_max = 60 # assuming each task is 60 seconds |
| 167 | +color_change_times = [0.25, 0.5, 0.75, 1.0] |
| 168 | +color_change_colors = ['green', 'yellow', 'orange', 'red'] |
| 169 | +
|
| 170 | +gui_x, gui_y = 700, 600 |
| 171 | +
|
| 172 | +class Window(QMainWindow): |
| 173 | + def __init__(self): |
| 174 | + super().__init__() |
| 175 | + self.setWindowTitle("Python ") |
| 176 | + self.win = QWidget() |
| 177 | + self.grid = QGridLayout() |
| 178 | +
|
| 179 | + self.UiComponents() |
| 180 | + self.win.setLayout(self.grid) |
| 181 | + self.win.setGeometry(0, 0, gui_x, gui_y) |
| 182 | + self.win.show() |
| 183 | +
|
| 184 | +
|
| 185 | + # self.status_sub = rospy.Subscriber('/mrsd/status', String, self.status_callback) |
| 186 | + # self.estop_pub = rospy.Publisher('/mrsd/estop', Bool, queue_size=10) |
| 187 | + # self.system_log_sub = rospy.Subscriber('/mrsd/system_log', String, self.system_log_callback) |
| 188 | +
|
| 189 | + def SystemTimerBlock(self): |
| 190 | + self.system_count = 0 |
| 191 | + self.system_start = False |
| 192 | + self.system_end = False |
| 193 | +
|
| 194 | + self.system_timer_button = QPushButton("Start System Timer", self) |
| 195 | + self.system_timer_button.setFixedSize(self.win.frameGeometry().width(),self.win.frameGeometry().height()//4) |
| 196 | + self.grid.addWidget(self.system_timer_button, 0, 2) |
| 197 | + self.system_timer_button.clicked.connect(self.system_timer_controller) |
| 198 | +
|
| 199 | + system_timer = QTimer(self) |
| 200 | + system_timer.timeout.connect(self.system_timer) |
| 201 | + system_timer.start(1000) # modify to match system needs |
| 202 | + |
| 203 | + def TaskTimerBlock(self): |
| 204 | + self.task_times = {} |
| 205 | + self.task_count = 0 |
| 206 | + self.task_start = False |
| 207 | + self.task_end = False |
| 208 | +
|
| 209 | + self.task_timer_button = QPushButton("Start Task Timer", self) |
| 210 | + self.task_timer_button.setFixedSize(self.win.frameGeometry().width(),self.win.frameGeometry().height()//4) |
| 211 | + self.grid.addWidget(self.task_timer_button, 1, 2) |
| 212 | + self.task_timer_button.clicked.connect(self.task_timer_controller) |
| 213 | +
|
| 214 | + task_timer = QTimer(self) |
| 215 | + task_timer.timeout.connect(self.task_timer) |
| 216 | + task_timer.start(1000) # modify to match system needs |
| 217 | +
|
| 218 | + self.task_times_label = QLabel("Task Times", self) |
| 219 | + self.grid.addWidget(self.task_times_label, 2, 2, 2, 1) |
| 220 | + self.task_times_label.setStyleSheet("border : 3px solid black") |
| 221 | + self.task_times_label.setFont(QFont('Times', 10)) |
| 222 | + self.task_times_label.setAlignment(Qt.AlignCenter) |
| 223 | +
|
| 224 | + def EStopBlock(self): |
| 225 | + self.estop_button = QPushButton("E-Stop", self) |
| 226 | + self.estop_button.setStyleSheet("background-color: red; border-radius: 15px") |
| 227 | + self.estop_button.setFixedWidth(self.win.frameGeometry().width()) |
| 228 | + self.estop_button.setFixedHeight(self.win.frameGeometry().height()//4) |
| 229 | + self.grid.addWidget(self.estop_button, 3, 0, 1, 1) |
| 230 | + self.estop_button.clicked.connect(self.estop_button_clicked) |
| 231 | +
|
| 232 | + def SystemLogsBlock(self): |
| 233 | + self.system_logs = QLabel("System Logs", self) |
| 234 | + self.grid.addWidget(self.system_logs, 1, 0, 2, 1) |
| 235 | + self.system_logs.setStyleSheet("border : 3px solid black") |
| 236 | + self.system_logs.setFont(QFont('Times', 8)) |
| 237 | + self.system_logs.setAlignment(Qt.AlignCenter) |
| 238 | +
|
| 239 | + def VisualizationBlock(self): |
| 240 | + self.pixmap = QPixmap('turtlesim.png') |
| 241 | + self.image_label = QLabel(self) |
| 242 | + self.image_label.setPixmap(self.pixmap) |
| 243 | + self.image_label.setStyleSheet("border : 3px solid black") |
| 244 | + self.grid.addWidget(self.image_label, 1, 1, 3, 1) |
| 245 | + |
| 246 | + def SystemStatusBlock(self): |
| 247 | + self.system_status = QLabel("System Status", self) |
| 248 | + self.system_status.setStyleSheet("border : 3px solid black") |
| 249 | + self.system_status.setFont(QFont('Times', 10)) |
| 250 | + self.system_status.setAlignment(Qt.AlignCenter) |
| 251 | + self.grid.addWidget(self.system_status, 0, 0, 1, 2) |
| 252 | +
|
| 253 | + def UiComponents(self): |
| 254 | + self.SystemTimerBlock() |
| 255 | + self.TaskTimerBlock() |
| 256 | + self.EStopBlock() |
| 257 | + self.SystemLogsBlock() |
| 258 | + self.VisualizationBlock() |
| 259 | + self.SystemStatusBlock() |
| 260 | +
|
| 261 | + |
| 262 | + def format_time(self, seconds): |
| 263 | + return f'{seconds // 60} Minutes {seconds % 60} Seconds' |
| 264 | + def change_system_color(self): |
| 265 | + if self.system_count/total_demo_time < color_change_times[0]: |
| 266 | + color = color_change_colors[0] |
| 267 | + elif self.system_count/total_demo_time < color_change_times[1]: |
| 268 | + color = color_change_colors[1] |
| 269 | + elif self.system_count/total_demo_time < color_change_times[2]: |
| 270 | + color = color_change_colors[2] |
| 271 | + else: |
| 272 | + color = color_change_colors[3] |
| 273 | + self.system_timer_button.setStyleSheet(f"background-color: {color}") |
| 274 | + def system_timer(self): |
| 275 | + if self.system_start == True: |
| 276 | + self.system_count += 1 |
| 277 | + text = self.format_time(self.system_count) |
| 278 | + self.system_timer_button.setText(text) |
| 279 | + self.change_system_color() |
| 280 | +
|
| 281 | + if self.system_end == True: |
| 282 | + self.system_start = False |
| 283 | + self.system_end = False |
| 284 | + def system_timer_controller(self): |
| 285 | + if self.system_start == False: |
| 286 | + self.system_start = True |
| 287 | + self.system_end = False |
| 288 | + else: |
| 289 | + self.system_start = False |
| 290 | + self.system_end = True |
| 291 | + self.system_timer_button.setText("Start System Timer") |
| 292 | + def change_task_color(self): |
| 293 | + if self.task_count/one_task_max < color_change_times[0]: |
| 294 | + color = color_change_colors[0] |
| 295 | + emoji = '😀' |
| 296 | + elif self.task_count/one_task_max < color_change_times[1]: |
| 297 | + color = color_change_colors[1] |
| 298 | + emoji = '😐' |
| 299 | + elif self.task_count/one_task_max < color_change_times[2]: |
| 300 | + color = color_change_colors[2] |
| 301 | + emoji = '😕' |
| 302 | + else: |
| 303 | + color = color_change_colors[3] |
| 304 | + emoji = '😡' |
| 305 | + self.task_timer_button.setStyleSheet(f"background-color: {color}") |
| 306 | + return emoji |
| 307 | + |
| 308 | + def task_timer(self): |
| 309 | + if self.task_start == True: |
| 310 | + self.task_count += 1 |
| 311 | + text = self.format_time(self.task_count) |
| 312 | + self.task_timer_button.setText(text) |
| 313 | + self.change_task_color() |
| 314 | +
|
| 315 | + if self.task_end == True: |
| 316 | + self.task_start = False |
| 317 | + self.task_end = False |
| 318 | + self.task_times[len(self.task_times)] = (self.task_count, self.change_task_color()) |
| 319 | + self.task_count = 0 |
| 320 | + self.task_times_label.setText(self.timer_label_format()) |
| 321 | + def task_timer_controller(self): |
| 322 | + if self.task_start == False: |
| 323 | + self.task_start = True |
| 324 | + self.task_end = False |
| 325 | + else: |
| 326 | + self.task_start = False |
| 327 | + self.task_end = True |
| 328 | + self.task_timer_button.setText("Start Task Timer") |
| 329 | + def timer_label_format(self): |
| 330 | + text = "" |
| 331 | + for i in range(len(self.task_times)): |
| 332 | + text += f"[{i+1}]:{self.task_times[i][1]}: {self.format_time(self.task_times[i][0])}\n" |
| 333 | + return text |
| 334 | + def estop_button_clicked(self): |
| 335 | + print("estop clicked") |
| 336 | + # self.estop_pub.publish(True) |
| 337 | +
|
| 338 | + def status_callback(self, msg): |
| 339 | + # updates the system status when status information is received |
| 340 | + if msg.data != '': |
| 341 | + self.system_status_label.setText(msg.data) |
| 342 | +
|
| 343 | + def system_output_callback(self, msg): |
| 344 | + # updates the system output when system information is received |
| 345 | + # should modify to display the system output in a more readable format for each team |
| 346 | + if msg.data != '': |
| 347 | + self.system_logs.setText(msg.data) |
| 348 | +
|
| 349 | +App = QApplication(sys.argv) |
| 350 | +window = Window() |
| 351 | +sys.exit(App.exec()) |
| 352 | + ``` |
| 353 | + |
| 354 | +</details> |
| 355 | + |
| 356 | + |
| 357 | +## Summary |
| 358 | +This article provides a walkthrough of basic code that uses PyQt5 for development of a GUI that is integrated with ROS. It highlights parts that need to be modified in your system level ROS code as well as suggests possible modifications. |
| 359 | + |
| 360 | +## See Also: |
| 361 | +- [PyQt5 Official Documentation](https://doc.qt.io/qtforpython-5/) |
| 362 | + |
| 363 | +## Further Reading |
| 364 | +- [PyQt5 Official Documentation](https://doc.qt.io/qtforpython-5/) |
| 365 | + |
0 commit comments