33 from __future__
import division
39 import py_trees_msgs.msg
as py_trees_msgs
47 import uuid_msgs.msg
as uuid_msgs
49 from .
import visibility
51 from .dotcode_behaviour
import RosBehaviourTreeDotcodeGenerator
52 from .dynamic_timeline
import DynamicTimeline
53 from .dynamic_timeline_listener
import DynamicTimelineListener
54 from .timeline_listener
import TimelineListener
62 from python_qt_binding
import loadUi
63 from python_qt_binding.QtCore
import QFile, QIODevice, QObject, Qt, Signal, QEvent, Slot
64 from python_qt_binding.QtGui
import QIcon, QImage, QPainter, QKeySequence
65 from python_qt_binding.QtSvg
import QSvgGenerator
67 from python_qt_binding.QtGui
import QFileDialog, QGraphicsView, QGraphicsScene, QWidget, QShortcut
69 from python_qt_binding.QtWidgets
import QFileDialog, QGraphicsView, QGraphicsScene, QWidget, QShortcut
71 from .
import qt_dotgraph
76 _deferred_fit_in_view = Signal()
77 _refresh_view = Signal()
78 _refresh_combo = Signal()
79 _message_changed = Signal()
80 _message_cleared = Signal()
81 _expected_type = py_trees_msgs.BehaviourTree()._type
82 _empty_topic =
"No valid topics available" 83 _unselected_topic =
"Not subscribing" 84 no_roscore_switch =
"--no-roscore" 87 """Event filter for the combo box. Will filter left mouse button presses, 88 calling a signal when they happen 94 :param Signal signal: signal that is emitted when a left mouse button press happens 100 if event.type() == QEvent.MouseButtonPress
and event.button() == Qt.LeftButton:
105 super(RosBehaviourTree, self).
__init__(context)
106 self.setObjectName(
'RosBehaviourTree')
108 parser = argparse.ArgumentParser()
109 RosBehaviourTree.add_arguments(parser,
False)
111 if not hasattr(context,
'argv'):
117 args = context.argv()
120 parsed_args = parser.parse_args(args)
146 rp = rospkg.RosPack()
147 ui_file = os.path.join(rp.get_path(
'rqt_py_trees'),
'resource',
'RosBehaviourTree.ui')
148 loadUi(ui_file, self.
_widget, {
'InteractiveGraphicsView': InteractiveGraphicsView})
149 self._widget.setObjectName(
'RosBehaviourTreeUi')
150 if hasattr(context,
'serial_number')
and context.serial_number() > 1:
151 self._widget.setWindowTitle(self._widget.windowTitle() + (
' (%d)' % context.serial_number()))
154 self._scene.setBackgroundBrush(Qt.white)
155 self._widget.graphics_view.setScene(self.
_scene)
159 self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme(
'zoom-original'))
160 self._widget.fit_in_view_push_button.pressed.connect(self.
_fit_in_view)
162 self._widget.load_bag_push_button.setIcon(QIcon.fromTheme(
'document-open'))
163 self._widget.load_bag_push_button.pressed.connect(self.
_load_bag)
164 self._widget.load_dot_push_button.setIcon(QIcon.fromTheme(
'document-open'))
165 self._widget.load_dot_push_button.pressed.connect(self.
_load_dot)
166 self._widget.save_dot_push_button.setIcon(QIcon.fromTheme(
'document-save-as'))
167 self._widget.save_dot_push_button.pressed.connect(self.
_save_dot)
168 self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme(
'document-save-as'))
169 self._widget.save_as_svg_push_button.pressed.connect(self.
_save_svg)
170 self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme(
'image'))
171 self._widget.save_as_image_push_button.pressed.connect(self.
_save_image)
173 for text
in visibility.combo_to_py_trees:
174 self._widget.visibility_level_combo_box.addItem(text)
175 self._widget.visibility_level_combo_box.setCurrentIndex(self.
visibility_level)
197 self._widget.topic_combo_box.activated.connect(self.
_choose_topic)
201 self._widget.previous_tool_button.pressed.connect(self.
_previous)
202 self._widget.previous_tool_button.setIcon(QIcon.fromTheme(
'go-previous'))
203 self._widget.next_tool_button.pressed.connect(self.
_next)
204 self._widget.next_tool_button.setIcon(QIcon.fromTheme(
'go-next'))
205 self._widget.first_tool_button.pressed.connect(self.
_first)
206 self._widget.first_tool_button.setIcon(QIcon.fromTheme(
'go-first'))
207 self._widget.last_tool_button.pressed.connect(self.
_last)
208 self._widget.last_tool_button.setIcon(QIcon.fromTheme(
'go-last'))
211 self._widget.play_tool_button.pressed.connect(self.
_play)
212 self._widget.play_tool_button.setIcon(QIcon.fromTheme(
'media-playback-start'))
213 self._widget.stop_tool_button.pressed.connect(self.
_stop)
214 self._widget.stop_tool_button.setIcon(QIcon.fromTheme(
'media-playback-stop'))
217 self._widget.first_tool_button.pressed.connect(self.
_stop)
218 self._widget.previous_tool_button.pressed.connect(self.
_stop)
219 self._widget.last_tool_button.pressed.connect(self.
_stop)
220 self._widget.next_tool_button.pressed.connect(self.
_stop)
223 next_shortcut_vi = QShortcut(QKeySequence(
"l"), self.
_widget)
224 next_shortcut_vi.activated.connect(self._widget.next_tool_button.pressed)
225 previous_shortcut_vi = QShortcut(QKeySequence(
"h"), self.
_widget)
226 previous_shortcut_vi.activated.connect(self._widget.previous_tool_button.pressed)
227 first_shortcut_vi = QShortcut(QKeySequence(
"^"), self.
_widget)
228 first_shortcut_vi.activated.connect(self._widget.first_tool_button.pressed)
229 last_shortcut_vi = QShortcut(QKeySequence(
"$"), self.
_widget)
230 last_shortcut_vi.activated.connect(self._widget.last_tool_button.pressed)
233 next_shortcut_emacs = QShortcut(QKeySequence(
"Ctrl+f"), self.
_widget)
234 next_shortcut_emacs.activated.connect(self._widget.next_tool_button.pressed)
235 previous_shortcut_emacs = QShortcut(QKeySequence(
"Ctrl+b"), self.
_widget)
236 previous_shortcut_emacs.activated.connect(self._widget.previous_tool_button.pressed)
237 first_shortcut_emacs = QShortcut(QKeySequence(
"Ctrl+a"), self.
_widget)
238 first_shortcut_emacs.activated.connect(self._widget.first_tool_button.pressed)
239 last_shortcut_emacs = QShortcut(QKeySequence(
"Ctrl+e"), self.
_widget)
240 last_shortcut_emacs.activated.connect(self._widget.last_tool_button.pressed)
257 self.
_set_timeline_buttons(first_snapshot=
False, previous_snapshot=
False, next_snapshot=
False, last_snapshot=
False)
261 self._deferred_fit_in_view.emit()
272 context.add_widget(self.
_widget)
275 context.setCentralWidget(self.
_widget)
279 elif parsed_args.latest_bag:
282 bag_dir = parsed_args.bag_dir
or os.getenv(
'ROS_HOME', os.path.expanduser(
'~/.ros')) +
'/behaviour_trees' 288 We match the combobox index to the visibility levels defined in py_trees.common.VisibilityLevel. 295 """Allows for the addition of arguments to the rqt_gui loading method 297 :param bool group: If set to false, this indicates that the function is 298 being called from the rqt_py_trees script as opposed to the inside 299 of rqt_gui.main. We use this to ensure that the same arguments can 300 be passed with and without the --no-roscore argument set. If it is 301 set, the rqt_gui code is bypassed. We need to make sure that all the 302 arguments are displayed with -h. 305 operate_object = parser
307 operate_object = parser.add_argument_group(
'Options for the rqt_py_trees viewer')
309 operate_object.add_argument(
'bag', action=
'store', nargs=
'?', help=
'Load this bag when the viewer starts')
310 operate_object.add_argument(
'-l',
'--latest-bag', action=
'store_true', help=
'Load the latest bag available in the bag directory. Bag files are expected to be under the bag directory in the following structure: year-month-day/behaviour_tree_hour-minute-second.bag. If this structure is not followed, the bag file which was most recently modified is used.')
311 operate_object.add_argument(
'--bag-dir', action=
'store', help=
'Specify the directory in which to look for bag files. The default is $ROS_HOME/behaviour_trees, if $ROS_HOME is set, or ~/.ros/behaviour_trees otherwise.')
312 operate_object.add_argument(
'-m',
'--by-time', action=
'store_true', help=
'The latest bag is defined by the time at which the file was last modified, rather than the date and time specified in the filename.')
313 operate_object.add_argument(RosBehaviourTree.no_roscore_switch, action=
'store_true', help=
'Run the viewer without roscore. It is only possible to view bag files if this is set.')
316 """Open the latest bag in the given directory 318 :param str bag_dir: the directory in which to look for bags 319 :param bool by_time: if true, the latest bag is the one with the latest 320 modification time, not the latest date-time specified by its filename 323 if not os.path.isdir(bag_dir):
324 rospy.logwarn(
"Requested bag directory {0} is invalid. Latest bag will not be loaded.".format(bag_dir))
328 for root, unused_dirnames, filenames
in os.walk(bag_dir, topdown=
True):
329 files.extend(fnmatch.filter(map(
lambda p: os.path.join(root, p), filenames),
'*.bag'))
332 rospy.logwarn(
"No files with extension .bag found in directory {0}".format(bag_dir))
338 re_str =
'.*\/\d{4}-\d{2}-\d{2}\/behaviour_tree_\d{2}-\d{2}-\d{2}.bag' 339 expr = re.compile(re_str)
340 valid = filter(
lambda f: expr.match(f), files)
347 latest_bag = sorted(valid)[-1]
350 latest_bag = sorted(files, cmp=
lambda x, y: cmp(os.path.getctime(x), os.path.getctime(y)))[-1]
356 Get the message in the list or bag that is being viewed that should be 362 msg = self._timeline_listener.msg
366 return py_trees_msgs.BehaviourTree()
if msg
is None else msg
369 """Updates the topic that is subscribed to based on changes to the combo box 370 text. If the topic is unchanged, nothing will happnen. Otherwise, the 371 old subscriber will be unregistered, and a new one initialised for the 372 updated topic. If the selected topic corresponds to the unselected 373 topic, the subscriber will be unregistered and a new one will not be 377 selected_topic = self._widget.topic_combo_box.currentText()
382 self._timeline.handle_close()
383 self._widget.timeline_graphics_view.setScene(
None)
391 Update the topics displayed in the combo box that the user can use to select 392 which topic they want to listen on for trees, filtered so that only 393 topics with the correct message type are shown. 397 self._widget.topic_combo_box.setEnabled(
False)
400 self._widget.topic_combo_box.clear()
401 topic_list = rospy.get_published_topics()
404 for topic_path, topic_type
in topic_list:
405 if topic_type == RosBehaviourTree._expected_type:
406 valid_topics.append(topic_path)
409 self._widget.topic_combo_box.addItem(RosBehaviourTree._empty_topic)
413 self._widget.topic_combo_box.addItem(RosBehaviourTree._unselected_topic)
414 for topic
in valid_topics:
415 self._widget.topic_combo_box.addItem(topic)
420 self._widget.topic_combo_box.setCurrentIndex(self._widget.topic_combo_box.count() - 1)
421 self.
_choose_topic(self._widget.topic_combo_box.currentIndex())
423 def _set_timeline_buttons(self, first_snapshot=None, previous_snapshot=None, next_snapshot=None, last_snapshot=None):
425 Allows timeline buttons to be enabled and disabled. 427 if first_snapshot
is not None:
428 self._widget.first_tool_button.setEnabled(first_snapshot)
429 if previous_snapshot
is not None:
430 self._widget.previous_tool_button.setEnabled(previous_snapshot)
431 if next_snapshot
is not None:
432 self._widget.next_tool_button.setEnabled(next_snapshot)
433 if last_snapshot
is not None:
434 self._widget.last_tool_button.setEnabled(last_snapshot)
438 Start a timer which will automatically call the next function every time its 439 duration is up. Only works if the current message is not the final one. 446 Helper function for the timer so that it can call the next function without 452 """Stop the play timer, if it exists. 455 self._play_timer.shutdown()
459 """Navigate to the first message. Activates the next and last buttons, disables 460 first and previous, and refreshes the view. Also changes the state to be 461 browsing the timeline. 464 self._timeline.navigate_start()
466 self.
_set_timeline_buttons(first_snapshot=
False, previous_snapshot=
False, next_snapshot=
True, last_snapshot=
True)
467 self._refresh_view.emit()
471 """Navigate to the previous message. Activates the next and last buttons, and 472 refreshes the view. If the current message is the second message, then 473 the first and previous buttons are disabled. Changes the state to be 474 browsing the timeline. 477 if self._timeline._timeline_frame.playhead == self._timeline._get_start_stamp():
481 self._timeline.navigate_previous()
486 if self._timeline._timeline_frame.playhead == self._timeline._get_end_stamp():
489 self._refresh_view.emit()
492 """Navigate to the last message. Activates the first and previous buttons, 493 disables next and last, and refreshes the view. The user is no longer 494 browsing the timeline after this is called. 497 self._timeline.navigate_end()
499 self.
_set_timeline_buttons(first_snapshot=
True, previous_snapshot=
True, next_snapshot=
False, last_snapshot=
False)
500 self._refresh_view.emit()
505 """Navigate to the next message. Activates the first and previous buttons. If 506 the current message is the second from last, disables the next and last 507 buttons, and stops browsing the timeline. 511 if self._timeline._timeline_frame.playhead == self._timeline._get_end_stamp():
515 self._timeline.navigate_next()
518 if self._timeline._timeline_frame.playhead == self._timeline._get_end_stamp():
522 self._play_timer.shutdown()
524 self._refresh_view.emit()
528 instance_settings.set_value(
'auto_fit_graph_check_box_state',
529 self._widget.auto_fit_graph_check_box.isChecked())
530 instance_settings.set_value(
'highlight_connections_check_box_state',
531 self._widget.highlight_connections_check_box.isChecked())
532 combo_text = self._widget.topic_combo_box.currentText()
534 instance_settings.set_value(
'combo_box_subscribed_topic', combo_text)
538 self._widget.auto_fit_graph_check_box.setChecked(
539 instance_settings.value(
'auto_fit_graph_check_box_state',
True)
in [
True,
'true'])
540 self._widget.highlight_connections_check_box.setChecked(
541 instance_settings.value(
'highlight_connections_check_box_state',
True)
in [
True,
'true'])
543 saved_visibility_level = instance_settings.value(
'visibility_level', 1)
544 except TypeError
as e:
545 self._widget.auto_fit_graph_check_box.setChecked(
True)
546 self._widget.highlight_connections_check_box.setChecked(
True)
548 saved_visibility_level = 1
549 rospy.logerr(
"Rqt PyTrees: incompatible qt app configuration found, try removing ~/.config/ros.org/rqt_gui.ini")
550 rospy.logerr(
"Rqt PyTrees: %s" % e)
551 self._widget.visibility_level_combo_box.setCurrentIndex(visibility.saved_setting_to_combo_index[saved_visibility_level])
557 """Refresh the graph view by regenerating the dotcode from the current message. 566 Get the dotcode for the given message, checking the cache for dotcode that 567 was previously generated, and adding to the cache if it wasn't there. 570 Mostly stolen from rqt_bag.MessageLoaderThread 572 :param py_trees_msgs.BehavoiurTree message 585 for behaviour
in reversed(message.behaviours):
587 if str(behaviour.parent_id) == str(uuid_msgs.UniqueID()):
589 tip_id = behaviour.tip_id
595 for behaviour
in message.behaviours:
596 if str(behaviour.own_id) == str(tip_id):
598 if '"' in behaviour.message:
599 print(
"%s" % termcolor.colored(
'[ERROR] found double quotes in the feedback message [%s]' % behaviour.message,
'red'))
600 behaviour.message = behaviour.message.replace(
'"',
'')
601 print(
"%s" % termcolor.colored(
'[ERROR] stripped to stop from crashing, but do catch the culprit! [%s]' % behaviour.message,
'red'))
603 key = str(message.header.stamp)
610 visible_behaviours = visibility.filter_behaviours_by_visibility_level(message.behaviours, self.
visibility_level)
613 dotcode = self.dotcode_generator.generate_dotcode(dotcode_factory=self.
dotcode_factory,
614 behaviours=visible_behaviours,
615 timestamp=message.header.stamp,
616 force_refresh=force_refresh
619 self._dotcode_cache_keys.append(key)
624 self._dotcode_cache_keys.remove(oldest)
639 new_scene = QGraphicsScene()
640 new_scene.setBackgroundBrush(Qt.white)
642 if self._widget.highlight_connections_check_box.isChecked():
653 for node_item
in nodes.itervalues():
654 new_scene.addItem(node_item)
655 for edge_items
in edges.itervalues():
656 for edge_item
in edge_items:
657 edge_item.add_to_scene(new_scene)
659 new_scene.setSceneRect(new_scene.itemsBoundingRect())
663 self._scene_cache_keys.append(key)
668 self._scene_cache_keys.remove(oldest)
673 self._widget.graphics_view.setScene(self.
_scene)
676 if self._widget.auto_fit_graph_check_box.isChecked():
680 """Activated when the window is resized. Will re-fit the behaviour tree in the 681 window, and update the size of the timeline scene rectangle so that it 687 self._timeline.setSceneRect(0, 0, self._widget.timeline_graphics_view.width() - 2, max(self._widget.timeline_graphics_view.height() - 2, self._timeline._timeline_frame._history_bottom))
690 """Should be called whenever the timeline changes. At the moment this is only 691 used to ensure that the first and previous buttons are correctly 692 disabled when a new message coming in on the timeline pushes the 693 playhead to be at the first message 696 if self._timeline._timeline_frame.playhead == self._timeline._get_start_stamp():
703 This function should be called when the message being viewed changes. Will 704 change the current message and update the view. Also ensures that the 705 timeline buttons are correctly set for the current position of the 706 playhead on the timeline. 708 if self._timeline._timeline_frame.playhead == self._timeline._get_end_stamp():
713 if self._timeline._timeline_frame.playhead == self._timeline._get_start_stamp():
718 self._refresh_view.emit()
722 This function should be called when the message being viewed was cleared. 723 Currently no situation where this happens? 728 """Decorator for ignoring right click events on mouse press 730 @functools.wraps(func)
732 if event.type() == QEvent.MouseButtonPress
and event.button() == Qt.RightButton:
740 Set the timeline to a dynamic timeline, listening to messages on the topic 741 selected in the combo box. 743 self.
_timeline = DynamicTimeline(self, publish_clock=
False)
746 self._widget.timeline_graphics_view.mouseReleaseEvent = self._timeline.on_mouse_up
747 self._widget.timeline_graphics_view.mouseMoveEvent = self._timeline.on_mouse_move
748 self._widget.timeline_graphics_view.wheelEvent = self._timeline.on_mousewheel
749 self._widget.timeline_graphics_view.setScene(self.
_timeline)
752 self._widget.timeline_graphics_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
753 self._widget.timeline_graphics_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
757 self._timeline.add_topic(self.
current_topic, py_trees_msgs.BehaviourTree)
768 self._timeline.navigate_end()
769 self._timeline._redraw_timeline(
None)
773 """Set the timeline of this object to a bag timeline, hooking the graphics view 774 into mouse and wheel functions of the timeline. 780 self._widget.timeline_graphics_view.mouseReleaseEvent = self._timeline.on_mouse_up
781 self._widget.timeline_graphics_view.mouseMoveEvent = self._timeline.on_mouse_move
782 self._widget.timeline_graphics_view.wheelEvent = self._timeline.on_mousewheel
783 self._widget.timeline_graphics_view.setScene(self.
_timeline)
786 self._widget.timeline_graphics_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
787 self._widget.timeline_graphics_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
791 self._timeline.add_bag(bag)
801 self._timeline.navigate_start()
804 """Load a bag from file. If no file name is given, a dialogue will pop up and 805 the user will be asked to select a file. If the bag file selected 806 doesn't have any valid topic, nothing will happen. If there are valid 807 topics, we load the bag and add a timeline for managing it. 810 if file_name
is None:
811 file_name, _ = QFileDialog.getOpenFileName(
813 self.tr(
'Open trees from bag file'),
815 self.tr(
'ROS bag (*.bag)'))
816 if file_name
is None or file_name ==
"":
819 rospy.loginfo(
"Reading bag from {0}".format(file_name))
822 topics = bag.get_type_and_topic_info()[1].keys()
824 for i
in range(0, len(bag.get_type_and_topic_info()[1].values())):
825 types.append(bag.get_type_and_topic_info()[1].values()[i][0])
828 for ind, tp
in enumerate(types):
829 if tp ==
'py_trees_msgs/BehaviourTree':
830 tree_topics.append(topics[ind])
832 if len(tree_topics) == 0:
833 rospy.logerr(
'Requested bag did not contain any valid topics.')
838 rospy.loginfo(
'Reading behaviour trees from topic {0}'.format(tree_topics[0]))
839 for unused_topic, msg, unused_t
in bag.read_messages(topics=[tree_topics[0]]):
840 self.message_list.append(msg)
843 self.
_set_timeline_buttons(first_snapshot=
True, previous_snapshot=
True, next_snapshot=
False, last_snapshot=
False)
845 self._refresh_view.emit()
848 if file_name
is None:
849 file_name, _ = QFileDialog.getOpenFileName(
851 self.tr(
'Open tree from DOT file'),
853 self.tr(
'DOT graph (*.dot)'))
854 if file_name
is None or file_name ==
'':
858 fhandle = open(file_name,
'rb')
859 dotcode = fhandle.read()
866 self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(),
870 file_name, _ = QFileDialog.getSaveFileName(self.
_widget,
871 self.tr(
'Save as DOT'),
873 self.tr(
'DOT graph (*.dot)'))
874 if file_name
is None or file_name ==
'':
877 dot_file = QFile(file_name)
878 if not dot_file.open(QIODevice.WriteOnly | QIODevice.Text):
885 file_name, _ = QFileDialog.getSaveFileName(
887 self.tr(
'Save as SVG'),
889 self.tr(
'Scalable Vector Graphic (*.svg)'))
890 if file_name
is None or file_name ==
'':
893 generator = QSvgGenerator()
894 generator.setFileName(file_name)
895 generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
897 painter = QPainter(generator)
898 painter.setRenderHint(QPainter.Antialiasing)
899 self._scene.render(painter)
903 file_name, _ = QFileDialog.getSaveFileName(
905 self.tr(
'Save as image'),
907 self.tr(
'Image (*.bmp *.jpg *.png *.tiff)'))
908 if file_name
is None or file_name ==
'':
911 img = QImage((self._scene.sceneRect().size() * 2.0).toSize(),
912 QImage.Format_ARGB32_Premultiplied)
913 painter = QPainter(img)
914 painter.setRenderHint(QPainter.Antialiasing)
915 self._scene.render(painter)
def restore_settings(self, plugin_settings, instance_settings)
def message_changed(self)
def no_right_click_press_event(self, func)
def __init__(self, context)
def message_cleared(self)
def get_current_message(self)
def _set_bag_timeline(self, bag)
def _update_visibility_level(self, visibility_level)
def __init__(self, signal)
def _timer_next(self, timer)
_tip_message
Get the tip, from the perspective of the root.
def save_settings(self, plugin_settings, instance_settings)
def _update_graph_view(self, dotcode)
def _load_dot(self, file_name=None)
def _generate_dotcode(self, message)
def timeline_changed(self)
def _set_dynamic_timeline(self)
def _refresh_tree_graph(self)
def _redraw_graph_view(self)
def open_latest_bag(self, bag_dir, by_time=False)
def _set_timeline_buttons(self, first_snapshot=None, previous_snapshot=None, next_snapshot=None, last_snapshot=None)
def _load_bag(self, file_name=None)
def _choose_topic(self, index)
def eventFilter(self, obj, event)
def add_arguments(parser, group=True)
def _resize_event(self, event)
def _update_combo_topics(self)