dynamic_timeline.py
Go to the documentation of this file.
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 import rospy
34 import time
35 import threading
36 import collections
37 import itertools
38 import bisect
39 
40 from python_qt_binding.QtCore import Qt, QTimer, qWarning, Signal
41 try: # indigo
42  from python_qt_binding.QtGui import QGraphicsScene, QMessageBox
43 except ImportError: # kinetic+ (pyqt5)
44  from python_qt_binding.QtWidgets import QGraphicsScene, QMessageBox
45 
46 from .dynamic_timeline_frame import DynamicTimelineFrame
47 from rqt_bag.message_listener_thread import MessageListenerThread
48 from .message_loader_thread import MessageLoaderThread
49 from rqt_bag.player import Player
50 from rqt_bag.recorder import Recorder
51 from rqt_bag.timeline_menu import TimelinePopupMenu
52 
53 from . import topic_helper
54 
55 
56 class DynamicTimeline(QGraphicsScene):
57  """
58  BagTimeline contains bag files, all information required to display the bag data visualization on the screen
59  Also handles events
60  """
61  status_bar_changed_signal = Signal()
62  selected_region_changed = Signal(rospy.Time, rospy.Time)
63  timeline_updated = Signal()
64  Topic = collections.namedtuple('Topic', ['subscriber', 'queue'])
65  Message = collections.namedtuple('Message', ['stamp', 'message'])
66 
67  def __init__(self, context, publish_clock):
68  """
69  :param context: plugin context hook to enable adding rqt_bag plugin widgets as ROS_GUI snapin panes, ''PluginContext''
70  """
71  super(DynamicTimeline, self).__init__()
72  # key is topic name, value is a named tuple of type Topic. The deque
73  # contains named tuples of type Message
74  self._topics = {}
75  # key is the data type, value is a list of topics with that type
76  self._datatypes = {}
77  self._topic_lock = threading.RLock()
78 
79  self.background_task = None # Display string
81 
82  # Playing / Recording
83  self._playhead_lock = threading.RLock()
84  self._max_play_speed = 1024.0 # fastest X play speed
85  self._min_play_speed = 1.0 / 1024.0 # slowest X play speed
86  self._play_speed = 0.0
87  self._play_all = False
89  self._playhead_positions = {} # topic -> position
90  self._message_loaders = {}
91  self._messages_cvs = {}
92  self._messages = {} # topic -> msg_data
93  self._message_listener_threads = {} # listener -> MessageListenerThread
94  self._player = False
95  self._publish_clock = publish_clock
96  self._recorder = None
97  self.last_frame = None
98  self.last_playhead = None
99  self.desired_playhead = None
100  self.wrap = True # should the playhead wrap when it reaches the end?
101  self.stick_to_end = False # should the playhead stick to the end?
102  self._play_timer = QTimer()
103  self._play_timer.timeout.connect(self.on_idle)
104  self._play_timer.setInterval(3)
105  self._redraw_timer = None # timer which can be used to periodically redraw the timeline
106 
107  # Plugin popup management
108  self._context = context
109  self.popups = {}
110  self._views = []
111  self._listeners = {}
112 
113  # Initialize scene
114  # the timeline renderer fixes use of black pens and fills, so ensure we fix white here for contrast.
115  # otherwise a dark qt theme will default it to black and the frame render pen will be unreadable
116  self.setBackgroundBrush(Qt.white)
118  self._timeline_frame.setPos(0, 0)
119  self.addItem(self._timeline_frame)
120 
122  self.__closed = False
123 
124  # timer to periodically redraw the timeline every so often
125  self._start_redraw_timer()
126 
127  def get_context(self):
128  """
129  :returns: the ROS_GUI context, 'PluginContext'
130  """
131  return self._context
132 
134  if not self._redraw_timer:
135  self._redraw_timer = rospy.Timer(rospy.Duration(0.5), self._redraw_timeline)
136 
138  if self._redraw_timer:
139  self._redraw_timer.shutdown()
140  self._redraw_timer = None
141 
142  def handle_close(self):
143  """
144  Cleans up the timeline, subscribers and any threads
145  """
146  if self.__closed:
147  return
148  else:
149  self.__closed = True
150  self._play_timer.stop()
151  for topic in self._get_topics():
152  self.stop_publishing(topic)
153  self._message_loaders[topic].stop()
154  if self._player:
155  self._player.stop()
156  if self._recorder:
157  self._recorder.stop()
158  if self.background_task is not None:
159  self.background_task_cancel = True
161  for topic in self._topics:
162  self._topics[topic][0].unregister() # unregister the subscriber
163  for frame in self._views:
164  if frame.parent():
165  self._context.remove_widget(frame)
166 
167  def _redraw_timeline(self, timer):
168  # save the playhead so that the redraw doesn't move it
169  playhead = self._timeline_frame._playhead
170  if playhead is None:
171  start = self._timeline_frame.play_region[0] is None
172  end = self._timeline_frame.play_region[1] is None
173  else:
174  start = True if playhead <= self._timeline_frame.play_region[0] else False
175  end = True if playhead >= self._timeline_frame.play_region[1] else False
176 
177  # do not keep setting this if you want the timeline to just grow.
178  self._timeline_frame._start_stamp = self._get_start_stamp()
179  self._timeline_frame._end_stamp = self._get_end_stamp()
180 
182 
183  if end:
184  # use play region instead of time.now to stop playhead going past
185  # the end of the region, which causes problems with
186  # navigate_previous
187  self._timeline_frame._set_playhead(self._timeline_frame.play_region[1])
188  elif start:
189  self._timeline_frame._set_playhead(self._timeline_frame.play_region[0])
190  else:
191  if playhead:
192  self._timeline_frame._set_playhead(playhead)
193 
194  self.timeline_updated.emit()
195 
196  def topic_callback(self, msg, topic):
197  """
198  Called whenever a message is received on any of the subscribed topics
199 
200  :param topic: the topic on which the message was received
201  :param msg: the message received
202 
203  """
204  message = self.Message(stamp=rospy.Time.now(), message=msg)
205  with self._topic_lock:
206  self._topics[topic].queue.append(message)
207 
208  # Invalidate entire cache for this topic
209  with self._timeline_frame.index_cache_cv:
210  self._timeline_frame.invalidated_caches.add(topic)
211  # if topic in self._timeline_frame.index_cache:
212  # del self._timeline_frame.index_cache[topic]
213  self._timeline_frame.index_cache_cv.notify()
214 
215  def add_topic(self, topic, type, num_msgs=20):
216  """Creates an indexing thread for the new topic. Fixes the borders and notifies
217  the indexing thread to index the new items bags
218 
219  :param topic: a topic to listen to
220  :param type: type of the topic to listen to
221  :param num_msgs: number of messages to retain on this topic. If this
222  value is exceeded, the oldest messages will be dropped
223 
224  :return: false if topic already in the timeline, true otherwise
225 
226  """
227  # first item in each sub-list is the name, second is type
228  if topic not in self._topics:
229  self._topics[topic] = self.Topic(subscriber=rospy.Subscriber(topic, type, queue_size=1, callback=self.topic_callback, callback_args=topic),
230  queue=collections.deque(maxlen=num_msgs))
231  self._datatypes.setdefault(type, []).append(topic)
232  else:
233  return False
234 
235  self._playhead_positions_cvs[topic] = threading.Condition()
236  self._messages_cvs[topic] = threading.Condition()
237  self._message_loaders[topic] = MessageLoaderThread(self, topic)
238 
239  self._timeline_frame._start_stamp = self._get_start_stamp()
240  self._timeline_frame._end_stamp = self._get_end_stamp()
241  self._timeline_frame.topics = self._get_topics()
242  self._timeline_frame._topics_by_datatype = self._get_topics_by_datatype()
243  # If this is the first bag, reset the timeline
245 
246  # Invalidate entire cache for this topic
247  with self._timeline_frame.index_cache_cv:
248  self._timeline_frame.invalidated_caches.add(topic)
249  if topic in self._timeline_frame.index_cache:
250  del self._timeline_frame.index_cache[topic]
251  self._timeline_frame.index_cache_cv.notify()
252 
253  return True
254 
255  # TODO Rethink API and if these need to be visible
256  def _get_start_stamp(self):
257  """
258 
259  :return: stamp of the first message received on any of the topics, or none if no messages have been received, ''rospy.Time''
260  """
261  with self._topic_lock:
262  start_stamp = None
263  for unused_topic_name, topic_tuple in self._topics.items():
264  topic_start_stamp = topic_helper.get_start_stamp(topic_tuple)
265  if topic_start_stamp is not None and (start_stamp is None or topic_start_stamp < start_stamp):
266  start_stamp = topic_start_stamp
267  return start_stamp
268 
269  def _get_end_stamp(self):
270  """
271 
272  :return: stamp of the last message received on any of the topics, or none if no messages have been received, ''rospy.Time''
273  """
274  with self._topic_lock:
275  end_stamp = None
276  for unused_topic_name, topic_tuple in self._topics.items():
277  topic_end_stamp = topic_helper.get_end_stamp(topic_tuple)
278  if topic_end_stamp is not None and (end_stamp is None or topic_end_stamp > end_stamp):
279  end_stamp = topic_end_stamp
280  return end_stamp
281 
282  def _get_topics(self):
283  """
284  :return: sorted list of topic names, ''list(str)''
285  """
286  with self._topic_lock:
287  topics = []
288  for topic in self._topics:
289  topics.append(topic)
290  return sorted(topics)
291 
293  """
294  :return: dict of list of topics for each datatype, ''dict(datatype:list(topic))''
295  """
296  with self._topic_lock:
297  return self._datatypes
298 
299  def get_datatype(self, topic):
300  """
301  :return: datatype associated with a topic, ''str''
302  :raises: if there are multiple datatypes assigned to a single topic, ''Exception''
303  """
304  with self._topic_lock:
305  topic_types = []
306  for datatype in self._datatypes:
307  if topic in self._datatypes[datatype]:
308  if len(topic_types) == 1:
309  raise Exception("Topic {0} had multiple datatypes ({1}) associated with it".format(topic, str(topic_types)))
310  topic_types.append(datatype._type)
311 
312  if not topic_types:
313  return None
314  else:
315  return topic_types[0]
316 
317  def get_entries(self, topics, start_stamp, end_stamp):
318  """
319  generator function for topic entries
320  :param topics: list of topics to query, ''list(str)''
321  :param start_stamp: stamp to start at, ''rospy.Time''
322  :param end_stamp: stamp to end at, ''rospy,Time''
323  :returns: messages on the given topics in chronological order, ''msg''
324  """
325  with self._topic_lock:
326  topic_entries = []
327  # make sure that we can handle a single topic as well
328  for topic in topics:
329  if not topic in self._topics:
330  rospy.logwarn("Dynamic Timeline : Topic {0} was not in the topic list. Skipping.".format(topic))
331  continue
332 
333  # don't bother with topics if they don't overlap the requested time range
334  topic_start_time = topic_helper.get_start_stamp(self._topics[topic])
335  if topic_start_time is not None and topic_start_time > end_stamp:
336  continue
337 
338  topic_end_time = topic_helper.get_end_stamp(self._topics[topic])
339  if topic_end_time is not None and topic_end_time < start_stamp:
340  continue
341 
342  topic_queue = self._topics[topic].queue
343  start_ind, first_entry = self._entry_at(start_stamp, topic_queue)
344  # entry returned might be the latest one before the start stamp
345  # if there isn't one exactly on the stamp - if so, start from
346  # the next entry
347  if first_entry.stamp < start_stamp:
348  start_ind += 1
349 
350  # entry at always returns entry at or before the given stamp, so
351  # no manipulation needed.
352  end_ind, last_entry = self._entry_at(end_stamp, topic_queue)
353 
354  topic_entries.extend(list(itertools.islice(topic_queue, start_ind, end_ind + 1)))
355 
356  for entry in sorted(topic_entries, key=lambda x: x.stamp):
357  yield entry
358 
359  def get_entries_with_bags(self, topic, start_stamp, end_stamp):
360  """
361  generator function for bag entries
362  :param topics: list of topics to query, ''list(str)''
363  :param start_stamp: stamp to start at, ''rospy.Time''
364  :param end_stamp: stamp to end at, ''rospy,Time''
365  :returns: tuple of (bag, entry) for the entries in the bag file, ''(rosbag.bag, msg)''
366  """
367  with self._bag_lock:
368  from rosbag import bag # for _mergesort
369 
370  bag_entries = []
371  bag_by_iter = {}
372  for b in self._bags:
373  bag_start_time = bag_helper.get_start_stamp(b)
374  if bag_start_time is not None and bag_start_time > end_stamp:
375  continue
376 
377  bag_end_time = bag_helper.get_end_stamp(b)
378  if bag_end_time is not None and bag_end_time < start_stamp:
379  continue
380 
381  connections = list(b._get_connections(topic))
382  it = iter(b._get_entries(connections, start_stamp, end_stamp))
383  bag_by_iter[it] = b
384  bag_entries.append(it)
385 
386  for entry, it in bag._mergesort(bag_entries, key=lambda entry: entry.time):
387  yield bag_by_iter[it], entry
388 
389  def _entry_at(self, t, queue):
390  """Get the entry and index in the queue at the given time.
391 
392  :param ``rospy.Time`` t: time to check
393  :param ``collections.deque`` queue: deque to look at
394 
395  :return: (index, Message) tuple. If there is no message in the queue at
396  the exact time, the previous index is returned. If the time is
397  before or after the first and last message times, the first or last
398  index and message are returned
399 
400  """
401  # Gives the index to insert into to retain a sorted queue. The topic queues
402  # should always be sorted due to time passing.
403 
404  # ind = bisect.bisect(queue, self.Message(stamp=t, message=''))
405  # Can't use bisect here in python3 unless the correct operators
406  # are defined for sorting, so do it manually
407  try:
408  ind = next(i for i, msg in enumerate(queue) if t < msg.stamp)
409  except StopIteration:
410  ind = len(queue)
411 
412  # first or last indices
413  if ind == len(queue):
414  return (ind - 1, queue[-1])
415  elif ind == 0:
416  return (0, queue[0])
417 
418  # non-end indices
419  cur = queue[ind]
420  if cur.stamp == t:
421  return (ind, cur)
422  else:
423  return (ind - 1, queue[ind - 1])
424 
425  def get_entry(self, t, topic):
426  """Get a message in the queues for a specific topic
427  :param ``rospy.Time`` t: time of the message to retrieve
428  :param str topic: the topic to be accessed
429  :return: message corresponding to time t and topic. If there is no
430  corresponding entry at exactly the given time, the latest entry
431  before the given time is returned. If the topic does not exist, or
432  there is no entry, None.
433 
434  """
435  with self._topic_lock:
436  entry = None
437  if topic in self._topics:
438  _, entry = self._entry_at(t, self._topics[topic].queue)
439 
440  return entry
441 
442  def get_entry_before(self, t):
443  """
444  Get the latest message before the given time on any of the topics
445  :param t: time, ``rospy.Time``
446  :return: tuple of (topic, entry) corresponding to time t, ``(str, msg)``
447  """
448  with self._topic_lock:
449  entry_topic, entry = None, None
450  for topic in self._topics:
451  _, topic_entry = self._entry_at(t - rospy.Duration(0,1), self._topics[topic].queue)
452  if topic_entry and (not entry or topic_entry.stamp > entry.stamp):
453  entry_topic, entry = topic, topic_entry
454 
455  return entry_topic, entry
456 
457  def get_entry_after(self, t):
458  """
459  Get the earliest message on any topic after the given time
460  :param t: time, ''rospy.Time''
461  :return: tuple of (bag, entry) corresponding to time t, ''(rosbag.bag, msg)''
462  """
463  with self._topic_lock:
464  entry_topic, entry = None, None
465  for topic in self._topics:
466  ind, _ = self._entry_at(t, self._topics[topic].queue)
467  # ind is the index of the entry at (if it exists) or before time
468  # t - we want the one after this. Make sure that the given index
469  # isn't out of bounds
470  ind = ind + 1 if ind + 1 < len(self._topics[topic].queue) else ind
471  topic_entry = self._topics[topic].queue[ind]
472  if topic_entry and (not entry or topic_entry.stamp < entry.stamp):
473  entry_topic, entry = topic, topic_entry
474 
475  return entry_topic, entry
476 
478  """
479  :return: time of the message after the current playhead position,''rospy.Time''
480  """
481  if self._timeline_frame.playhead is None:
482  return None
483 
484  _, entry = self.get_entry_after(self._timeline_frame.playhead)
485  if entry is None:
486  return self._timeline_frame._end_stamp
487 
488  return entry.stamp
489 
491  """
492  :return: time of the message before the current playhead position,''rospy.Time''
493  """
494  if self._timeline_frame.playhead is None:
495  return None
496 
497  _, entry = self.get_entry_before(self._timeline_frame.playhead)
498  if entry is None:
499  return self._timeline_frame._start_stamp
500 
501  return entry.stamp
502 
503  def resume(self):
504  if (self._player):
505  self._player.resume()
506 
507 
508 
509  def start_background_task(self, background_task):
510  """
511  Verify that a background task is not currently running before starting a new one
512  :param background_task: name of the background task, ''str''
513  """
514  if self.background_task is not None:
515  QMessageBox(QMessageBox.Warning, 'Exclamation', 'Background operation already running:\n\n%s' % self.background_task, QMessageBox.Ok).exec_()
516  return False
517 
518  self.background_task = background_task
519  self.background_task_cancel = False
520  return True
521 
523  self.background_task = None
524 
525  def copy_region_to_bag(self, filename):
526  if len(self._bags) > 0:
527  self._export_region(filename, self._timeline_frame.topics, self._timeline_frame.play_region[0], self._timeline_frame.play_region[1])
528 
529  def _export_region(self, path, topics, start_stamp, end_stamp):
530  """
531  Starts a thread to save the current selection to a new bag file
532  :param path: filesystem path to write to, ''str''
533  :param topics: topics to write to the file, ''list(str)''
534  :param start_stamp: start of area to save, ''rospy.Time''
535  :param end_stamp: end of area to save, ''rospy.Time''
536  """
537  if not self.start_background_task('Copying messages to "%s"' % path):
538  return
539  # TODO implement a status bar area with information on the current save status
540  bag_entries = list(self.get_entries_with_bags(topics, start_stamp, end_stamp))
541 
542  if self.background_task_cancel:
543  return
544 
545  # Get the total number of messages to copy
546  total_messages = len(bag_entries)
547 
548  # If no messages, prompt the user and return
549  if total_messages == 0:
550  QMessageBox(QMessageBox.Warning, 'rqt_bag', 'No messages found', QMessageBox.Ok).exec_()
551  self.stop_background_task()
552  return
553 
554  # Open the path for writing
555  try:
556  export_bag = rosbag.Bag(path, 'w')
557  except Exception:
558  QMessageBox(QMessageBox.Warning, 'rqt_bag', 'Error opening bag file [%s] for writing' % path, QMessageBox.Ok).exec_()
559  self.stop_background_task()
560  return
561 
562  # Run copying in a background thread
563  self._export_thread = threading.Thread(target=self._run_export_region, args=(export_bag, topics, start_stamp, end_stamp, bag_entries))
564  self._export_thread.start()
565 
566  def _run_export_region(self, export_bag, topics, start_stamp, end_stamp, bag_entries):
567  """
568  Threaded function that saves the current selection to a new bag file
569  :param export_bag: bagfile to write to, ''rosbag.bag''
570  :param topics: topics to write to the file, ''list(str)''
571  :param start_stamp: start of area to save, ''rospy.Time''
572  :param end_stamp: end of area to save, ''rospy.Time''
573  """
574  total_messages = len(bag_entries)
575  update_step = max(1, total_messages / 100)
576  message_num = 1
577  progress = 0
578  # Write out the messages
579  for bag, entry in bag_entries:
580  if self.background_task_cancel:
581  break
582  try:
583  topic, msg, t = self.read_message(bag, entry.position)
584  export_bag.write(topic, msg, t)
585  except Exception as ex:
586  qWarning('Error exporting message at position %s: %s' % (str(entry.position), str(ex)))
587  export_bag.close()
588  self.stop_background_task()
589  return
590 
591  if message_num % update_step == 0 or message_num == total_messages:
592  new_progress = int(100.0 * (float(message_num) / total_messages))
593  if new_progress != progress:
594  progress = new_progress
595  if not self.background_task_cancel:
596  self.background_progress = progress
597  self.status_bar_changed_signal.emit()
598 
599  message_num += 1
600 
601  # Close the bag
602  try:
603  self.background_progress = 0
604  self.status_bar_changed_signal.emit()
605  export_bag.close()
606  except Exception as ex:
607  QMessageBox(QMessageBox.Warning, 'rqt_bag', 'Error closing bag file [%s]: %s' % (export_bag.filename, str(ex)), QMessageBox.Ok).exec_()
608  self.stop_background_task()
609 
610  def read_message(self, topic, position):
611  with self._topic_lock:
612  return self.get_entry(position, topic).message
613 
614  def on_mouse_down(self, event):
615  """
616  When the user clicks down in the timeline.
617  """
618  if event.buttons() == Qt.LeftButton:
619  self._timeline_frame.on_left_down(event)
620  elif event.buttons() == Qt.MidButton:
621  self._timeline_frame.on_middle_down(event)
622  elif event.buttons() == Qt.RightButton:
623  topic = self._timeline_frame.map_y_to_topic(event.y())
624  TimelinePopupMenu(self, event, topic)
625 
626  def on_mouse_up(self, event):
627  self._timeline_frame.on_mouse_up(event)
628 
629  def on_mouse_move(self, event):
630  self._timeline_frame.on_mouse_move(event)
631 
632  def on_mousewheel(self, event):
633  self._timeline_frame.on_mousewheel(event)
634 
635  # Zooming
636 
637  def zoom_in(self):
638  self._timeline_frame.zoom_in()
639 
640  def zoom_out(self):
642 
643  def reset_zoom(self):
645 
648 
651 
652 
653  def is_publishing(self, topic):
654  return self._player and self._player.is_publishing(topic)
655 
656  def start_publishing(self, topic):
657  if not self._player and not self._create_player():
658  return False
659 
660  self._player.start_publishing(topic)
661  return True
662 
663  def stop_publishing(self, topic):
664  if not self._player:
665  return False
666 
667  self._player.stop_publishing(topic)
668  return True
669 
670  def _create_player(self):
671  if not self._player:
672  try:
673  self._player = Player(self)
674  if self._publish_clock:
675  self._player.start_clock_publishing()
676  except Exception as ex:
677  qWarning('Error starting player; aborting publish: %s' % str(ex))
678  return False
679 
680  return True
681 
682  def set_publishing_state(self, start_publishing):
683  if start_publishing:
684  for topic in self._timeline_frame.topics:
685  if not self.start_publishing(topic):
686  break
687  else:
688  for topic in self._timeline_frame.topics:
689  self.stop_publishing(topic)
690 
691  # property: play_all
692  def _get_play_all(self):
693  return self._play_all
694 
695  def _set_play_all(self, play_all):
696  if play_all == self._play_all:
697  return
698 
699  self._play_all = not self._play_all
700 
701  self.last_frame = None
702  self.last_playhead = None
703  self.desired_playhead = None
704 
705  play_all = property(_get_play_all, _set_play_all)
706 
707  def toggle_play_all(self):
708  self.play_all = not self.play_all
709 
710 
711  def on_idle(self):
712  self._step_playhead()
713 
714  def _step_playhead(self):
715  """
716  moves the playhead to the next position based on the desired position
717  """
718  # Reset when the playing mode switchs
719  if self._timeline_frame.playhead != self.last_playhead:
720  self.last_frame = None
721  self.last_playhead = None
722  self.desired_playhead = None
723 
724  if self._play_all:
725  self.step_next_message()
726  else:
727  self.step_fixed()
728 
729  def step_fixed(self):
730  """
731  Moves the playhead a fixed distance into the future based on the current play speed
732  """
733  if self.play_speed == 0.0 or not self._timeline_frame.playhead:
734  self.last_frame = None
735  self.last_playhead = None
736  return
737 
738  now = rospy.Time.from_sec(time.time())
739  if self.last_frame:
740  # Get new playhead
741  if self.stick_to_end:
742  new_playhead = self.end_stamp
743  else:
744  new_playhead = self._timeline_frame.playhead + rospy.Duration.from_sec((now - self.last_frame).to_sec() * self.play_speed)
745 
746  start_stamp, end_stamp = self._timeline_frame.play_region
747 
748  if new_playhead > end_stamp:
749  if self.wrap:
750  if self.play_speed > 0.0:
751  new_playhead = start_stamp
752  else:
753  new_playhead = end_stamp
754  else:
755  new_playhead = end_stamp
756 
757  if self.play_speed > 0.0:
758  self.stick_to_end = True
759 
760  elif new_playhead < start_stamp:
761  if self.wrap:
762  if self.play_speed < 0.0:
763  new_playhead = end_stamp
764  else:
765  new_playhead = start_stamp
766  else:
767  new_playhead = start_stamp
768 
769  # Update the playhead
770  self._timeline_frame.playhead = new_playhead
771 
772  self.last_frame = now
773  self.last_playhead = self._timeline_frame.playhead
774 
775  def step_next_message(self):
776  """
777  Move the playhead to the next message
778  """
779  if self.play_speed <= 0.0 or not self._timeline_frame.playhead:
780  self.last_frame = None
781  self.last_playhead = None
782  return
783 
784  if self.last_frame:
785  if not self.desired_playhead:
786  self.desired_playhead = self._timeline_frame.playhead
787  else:
788  delta = rospy.Time.from_sec(time.time()) - self.last_frame
789  if delta > rospy.Duration.from_sec(0.1):
790  delta = rospy.Duration.from_sec(0.1)
791  self.desired_playhead += delta
792 
793  # Get the occurrence of the next message
794  next_message_time = self.get_next_message_time()
795 
796  if next_message_time < self.desired_playhead:
797  self._timeline_frame.playhead = next_message_time
798  else:
799  self._timeline_frame.playhead = self.desired_playhead
800 
801  self.last_frame = rospy.Time.from_sec(time.time())
802  self.last_playhead = self._timeline_frame.playhead
803 
804  # Recording
805  def record_bag(self, filename, all=True, topics=[], regex=False, limit=0):
806  try:
807  self._recorder = Recorder(filename, bag_lock=self._bag_lock, all=all, topics=topics, regex=regex, limit=limit)
808  except Exception as ex:
809  qWarning('Error opening bag for recording [%s]: %s' % (filename, str(ex)))
810  return
811 
813 
814  self.add_bag(self._recorder.bag)
815 
816  self._recorder.start()
817 
818  self.wrap = False
819  self._timeline_frame._index_cache_thread.period = 0.1
820 
821  self.update()
822 
823  def toggle_recording(self):
824  if self._recorder:
825  self._recorder.toggle_paused()
826  self.update()
827 
828  def _message_recorded(self, topic, msg, t):
829  if self._timeline_frame._start_stamp is None:
830  self._timeline_frame._start_stamp = t
831  self._timeline_frame._end_stamp = t
832  self._timeline_frame._playhead = t
833  elif self._timeline_frame._end_stamp is None or t > self._timeline_frame._end_stamp:
834  self._timeline_frame._end_stamp = t
835 
836  if not self._timeline_frame.topics or topic not in self._timeline_frame.topics:
837  self._timeline_frame.topics = self._get_topics()
838  self._timeline_frame._topics_by_datatype = self._get_topics_by_datatype()
839 
840  self._playhead_positions_cvs[topic] = threading.Condition()
841  self._messages_cvs[topic] = threading.Condition()
842  self._message_loaders[topic] = MessageLoaderThread(self, topic)
843 
844  if self._timeline_frame._stamp_left is None:
845  self.reset_zoom()
846 
847  # Notify the index caching thread that it has work to do
848  with self._timeline_frame.index_cache_cv:
849  self._timeline_frame.invalidated_caches.add(topic)
850  self._timeline_frame.index_cache_cv.notify()
851 
852  if topic in self._listeners:
853  for listener in self._listeners[topic]:
854  try:
855  listener.timeline_changed()
856  except Exception as ex:
857  qWarning('Error calling timeline_changed on %s: %s' % (type(listener), str(ex)))
858 
859  # Views / listeners
860  def add_view(self, topic, frame):
861  self._views.append(frame)
862 
863  def has_listeners(self, topic):
864  return topic in self._listeners
865 
866  def add_listener(self, topic, listener):
867  self._listeners.setdefault(topic, []).append(listener)
868 
869  self._message_listener_threads[(topic, listener)] = MessageListenerThread(self, topic, listener)
870  # Notify the message listeners
871  self._message_loaders[topic].reset()
872  with self._playhead_positions_cvs[topic]:
873  self._playhead_positions_cvs[topic].notify_all()
874 
875  self.update()
876 
877  def remove_listener(self, topic, listener):
878  topic_listeners = self._listeners.get(topic)
879  if topic_listeners is not None and listener in topic_listeners:
880  topic_listeners.remove(listener)
881 
882  if len(topic_listeners) == 0:
883  del self._listeners[topic]
884 
885  # Stop the message listener thread
886  if (topic, listener) in self._message_listener_threads:
887  self._message_listener_threads[(topic, listener)].stop()
888  del self._message_listener_threads[(topic, listener)]
889  self.update()
890 
891 
892 
893  # property: play_speed
894  def _get_play_speed(self):
895  if self._timeline_frame._paused:
896  return 0.0
897  return self._play_speed
898 
899  def _set_play_speed(self, play_speed):
900  if play_speed == self._play_speed:
901  return
902 
903  if play_speed > 0.0:
904  self._play_speed = min(self._max_play_speed, max(self._min_play_speed, play_speed))
905  elif play_speed < 0.0:
906  self._play_speed = max(-self._max_play_speed, min(-self._min_play_speed, play_speed))
907  else:
908  self._play_speed = play_speed
909 
910  if self._play_speed < 1.0:
911  self.stick_to_end = False
912 
913  self.update()
914  play_speed = property(_get_play_speed, _set_play_speed)
915 
916  def toggle_play(self):
917  if self._play_speed != 0.0:
918  self.play_speed = 0.0
919  else:
920  self.play_speed = 1.0
921 
922  def navigate_play(self):
923  self.play_speed = 1.0
924  self.last_frame = rospy.Time.from_sec(time.time())
925  self.last_playhead = self._timeline_frame.playhead
926  self._play_timer.start()
927 
928  def navigate_stop(self):
929  self.play_speed = 0.0
930  self._play_timer.stop()
931 
932  def navigate_previous(self):
933  self.navigate_stop()
934  self._timeline_frame.playhead = self.get_previous_message_time()
935  self.last_playhead = self._timeline_frame.playhead
936 
937  def navigate_next(self):
938  self.navigate_stop()
939  self._timeline_frame.playhead = self.get_next_message_time()
940  self.last_playhead = self._timeline_frame.playhead
941 
942  def navigate_rewind(self):
943  if self._play_speed < 0.0:
944  new_play_speed = self._play_speed * 2.0
945  elif self._play_speed == 0.0:
946  new_play_speed = -1.0
947  else:
948  new_play_speed = self._play_speed * 0.5
949 
950  self.play_speed = new_play_speed
951 
953  if self._play_speed > 0.0:
954  new_play_speed = self._play_speed * 2.0
955  elif self._play_speed == 0.0:
956  new_play_speed = 2.0
957  else:
958  new_play_speed = self._play_speed * 0.5
959 
960  self.play_speed = new_play_speed
961 
962  def navigate_start(self):
963  self._timeline_frame.playhead = self._timeline_frame.play_region[0]
964 
965  def navigate_end(self):
966  self._timeline_frame.playhead = self._timeline_frame.play_region[1]
rqt_py_trees.dynamic_timeline.DynamicTimeline.set_publishing_state
def set_publishing_state(self, start_publishing)
Definition: dynamic_timeline.py:682
rqt_py_trees.dynamic_timeline.DynamicTimeline._redraw_timeline
def _redraw_timeline(self, timer)
Definition: dynamic_timeline.py:167
rqt_py_trees.dynamic_timeline.DynamicTimeline.on_idle
def on_idle(self)
Playing.
Definition: dynamic_timeline.py:711
rqt_py_trees.dynamic_timeline.DynamicTimeline._max_play_speed
_max_play_speed
Definition: dynamic_timeline.py:84
rqt_py_trees.dynamic_timeline.DynamicTimeline._min_play_speed
_min_play_speed
Definition: dynamic_timeline.py:85
rqt_py_trees.dynamic_timeline.DynamicTimeline._messages
_messages
Definition: dynamic_timeline.py:92
rqt_py_trees.dynamic_timeline.DynamicTimeline._redraw_timer
_redraw_timer
Definition: dynamic_timeline.py:105
rosbag::Bag
rqt_py_trees.dynamic_timeline.DynamicTimeline._recorder
_recorder
Definition: dynamic_timeline.py:96
rqt_py_trees.dynamic_timeline.DynamicTimeline.__closed
__closed
Definition: dynamic_timeline.py:122
rqt_py_trees.dynamic_timeline.DynamicTimeline.add_view
def add_view(self, topic, frame)
Definition: dynamic_timeline.py:860
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_previous_message_time
def get_previous_message_time(self)
Definition: dynamic_timeline.py:490
rqt_bag::recorder
rqt_py_trees.dynamic_timeline.DynamicTimeline._entry_at
def _entry_at(self, t, queue)
Definition: dynamic_timeline.py:389
rqt_py_trees.dynamic_timeline.DynamicTimeline.on_mouse_move
def on_mouse_move(self, event)
Definition: dynamic_timeline.py:629
rqt_py_trees.dynamic_timeline.DynamicTimeline.play_all
play_all
Definition: dynamic_timeline.py:705
rqt_py_trees.dynamic_timeline.DynamicTimeline._start_redraw_timer
def _start_redraw_timer(self)
Definition: dynamic_timeline.py:133
rqt_bag::timeline_menu::TimelinePopupMenu
rqt_py_trees.dynamic_timeline.DynamicTimeline._create_player
def _create_player(self)
Definition: dynamic_timeline.py:670
rqt_py_trees.dynamic_timeline.DynamicTimeline.copy_region_to_bag
def copy_region_to_bag(self, filename)
Definition: dynamic_timeline.py:525
rqt_py_trees.dynamic_timeline.DynamicTimeline.popups
popups
Definition: dynamic_timeline.py:109
rqt_py_trees.dynamic_timeline.DynamicTimeline._timeline_frame
_timeline_frame
Definition: dynamic_timeline.py:117
rqt_py_trees.dynamic_timeline.DynamicTimeline._message_recorded
def _message_recorded(self, topic, msg, t)
Definition: dynamic_timeline.py:828
rqt_py_trees.dynamic_timeline.DynamicTimeline.zoom_in
def zoom_in(self)
Definition: dynamic_timeline.py:637
rqt_py_trees.dynamic_timeline.DynamicTimeline.is_publishing
def is_publishing(self, topic)
Publishing.
Definition: dynamic_timeline.py:653
rqt_py_trees.dynamic_timeline.DynamicTimeline.step_next_message
def step_next_message(self)
Definition: dynamic_timeline.py:775
rqt_py_trees.dynamic_timeline.DynamicTimeline._get_start_stamp
def _get_start_stamp(self)
Definition: dynamic_timeline.py:256
rqt_py_trees.dynamic_timeline.DynamicTimeline._run_export_region
def _run_export_region(self, export_bag, topics, start_stamp, end_stamp, bag_entries)
Definition: dynamic_timeline.py:566
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_context
def get_context(self)
Definition: dynamic_timeline.py:127
rqt_py_trees.dynamic_timeline.DynamicTimeline.record_bag
def record_bag(self, filename, all=True, topics=[], regex=False, limit=0)
Definition: dynamic_timeline.py:805
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_start
def navigate_start(self)
Definition: dynamic_timeline.py:962
rqt_py_trees.dynamic_timeline.DynamicTimeline._set_play_speed
def _set_play_speed(self, play_speed)
Definition: dynamic_timeline.py:899
rqt_py_trees.dynamic_timeline.DynamicTimeline._playhead_lock
_playhead_lock
Definition: dynamic_timeline.py:83
rqt_py_trees.dynamic_timeline.DynamicTimeline.Topic
Topic
Definition: dynamic_timeline.py:64
rqt_py_trees.dynamic_timeline.DynamicTimeline.background_task_cancel
background_task_cancel
Definition: dynamic_timeline.py:80
rqt_py_trees.dynamic_timeline.DynamicTimeline.on_mouse_up
def on_mouse_up(self, event)
Definition: dynamic_timeline.py:626
rqt_bag::recorder::Recorder
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_entry_after
def get_entry_after(self, t)
Definition: dynamic_timeline.py:457
rqt_bag::player::Player
rqt_py_trees.dynamic_timeline.DynamicTimeline.stop_background_task
def stop_background_task(self)
Definition: dynamic_timeline.py:522
rqt_py_trees.dynamic_timeline.DynamicTimeline.background_task
background_task
Definition: dynamic_timeline.py:79
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_fastforward
def navigate_fastforward(self)
Definition: dynamic_timeline.py:952
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_next_message_time
def get_next_message_time(self)
Definition: dynamic_timeline.py:477
rqt_py_trees.dynamic_timeline.DynamicTimeline.toggle_play
def toggle_play(self)
Definition: dynamic_timeline.py:916
rqt_py_trees.dynamic_timeline.DynamicTimeline.toggle_play_all
def toggle_play_all(self)
Definition: dynamic_timeline.py:707
rqt_py_trees.dynamic_timeline.DynamicTimeline._play_timer
_play_timer
Definition: dynamic_timeline.py:102
rqt_bag::player
rqt_py_trees.dynamic_timeline.DynamicTimeline._playhead_positions
_playhead_positions
Definition: dynamic_timeline.py:89
rqt_py_trees.dynamic_timeline.DynamicTimeline.stop_publishing
def stop_publishing(self, topic)
Definition: dynamic_timeline.py:663
rqt_py_trees.dynamic_timeline.DynamicTimeline.step_fixed
def step_fixed(self)
Definition: dynamic_timeline.py:729
rqt_py_trees.dynamic_timeline.DynamicTimeline._message_loaders
_message_loaders
Definition: dynamic_timeline.py:90
rqt_py_trees.dynamic_timeline.DynamicTimeline.last_playhead
last_playhead
Definition: dynamic_timeline.py:98
rqt_py_trees.dynamic_timeline.DynamicTimeline.add_listener
def add_listener(self, topic, listener)
Definition: dynamic_timeline.py:866
rqt_py_trees.dynamic_timeline.DynamicTimeline.translate_timeline_left
def translate_timeline_left(self)
Definition: dynamic_timeline.py:646
rqt_py_trees.dynamic_timeline.DynamicTimeline._export_thread
_export_thread
Definition: dynamic_timeline.py:563
rqt_py_trees.dynamic_timeline.DynamicTimeline._get_end_stamp
def _get_end_stamp(self)
Definition: dynamic_timeline.py:269
rqt_py_trees.dynamic_timeline.DynamicTimeline
Definition: dynamic_timeline.py:56
rqt_py_trees.dynamic_timeline.DynamicTimeline.start_background_task
def start_background_task(self, background_task)
Copy messages to...
Definition: dynamic_timeline.py:509
rqt_py_trees.dynamic_timeline.DynamicTimeline._set_play_all
def _set_play_all(self, play_all)
Definition: dynamic_timeline.py:695
rqt_py_trees.dynamic_timeline.DynamicTimeline.last_frame
last_frame
Definition: dynamic_timeline.py:97
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_entry
def get_entry(self, t, topic)
Definition: dynamic_timeline.py:425
rqt_py_trees.dynamic_timeline.DynamicTimeline.on_mouse_down
def on_mouse_down(self, event)
Definition: dynamic_timeline.py:614
rqt_py_trees.dynamic_timeline.DynamicTimeline._get_topics_by_datatype
def _get_topics_by_datatype(self)
Definition: dynamic_timeline.py:292
rqt_py_trees.dynamic_timeline.DynamicTimeline.on_mousewheel
def on_mousewheel(self, event)
Definition: dynamic_timeline.py:632
rqt_py_trees.dynamic_timeline.DynamicTimeline.wrap
wrap
Definition: dynamic_timeline.py:100
rqt_py_trees.dynamic_timeline.DynamicTimeline._get_play_speed
def _get_play_speed(self)
Playhead.
Definition: dynamic_timeline.py:894
rqt_py_trees.dynamic_timeline.DynamicTimeline._player
_player
Definition: dynamic_timeline.py:94
rqt_py_trees.dynamic_timeline.DynamicTimeline.Message
Message
Definition: dynamic_timeline.py:65
rqt_py_trees.dynamic_timeline.DynamicTimeline._export_region
def _export_region(self, path, topics, start_stamp, end_stamp)
Definition: dynamic_timeline.py:529
rqt_py_trees.dynamic_timeline.DynamicTimeline._messages_cvs
_messages_cvs
Definition: dynamic_timeline.py:91
rqt_py_trees.dynamic_timeline.DynamicTimeline._topic_lock
_topic_lock
Definition: dynamic_timeline.py:77
rqt_py_trees.dynamic_timeline.DynamicTimeline.read_message
def read_message(self, topic, position)
Definition: dynamic_timeline.py:610
rqt_py_trees.dynamic_timeline.DynamicTimeline.topic_callback
def topic_callback(self, msg, topic)
Definition: dynamic_timeline.py:196
rqt_py_trees.dynamic_timeline.DynamicTimeline._context
_context
Definition: dynamic_timeline.py:108
rqt_py_trees.dynamic_timeline.DynamicTimeline.status_bar_changed_signal
status_bar_changed_signal
Definition: dynamic_timeline.py:61
rqt_py_trees.dynamic_timeline.DynamicTimeline.remove_listener
def remove_listener(self, topic, listener)
Definition: dynamic_timeline.py:877
rqt_py_trees.dynamic_timeline.DynamicTimeline._datatypes
_datatypes
Definition: dynamic_timeline.py:76
rqt_py_trees.dynamic_timeline.DynamicTimeline._publish_clock
_publish_clock
Definition: dynamic_timeline.py:95
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_next
def navigate_next(self)
Definition: dynamic_timeline.py:937
rqt_py_trees.dynamic_timeline.DynamicTimeline._message_listener_threads
_message_listener_threads
Definition: dynamic_timeline.py:93
rqt_py_trees.dynamic_timeline.DynamicTimeline.play_speed
play_speed
Definition: dynamic_timeline.py:914
rqt_py_trees.message_loader_thread.MessageLoaderThread
Definition: message_loader_thread.py:36
rqt_py_trees.dynamic_timeline.DynamicTimeline._get_play_all
def _get_play_all(self)
Definition: dynamic_timeline.py:692
rqt_py_trees.dynamic_timeline.DynamicTimeline.translate_timeline_right
def translate_timeline_right(self)
Definition: dynamic_timeline.py:649
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_entry_before
def get_entry_before(self, t)
Definition: dynamic_timeline.py:442
rqt_py_trees.dynamic_timeline.DynamicTimeline.desired_playhead
desired_playhead
Definition: dynamic_timeline.py:99
rqt_bag::message_listener_thread::MessageListenerThread
rqt_py_trees.dynamic_timeline.DynamicTimeline.add_topic
def add_topic(self, topic, type, num_msgs=20)
Definition: dynamic_timeline.py:215
rqt_py_trees.dynamic_timeline.DynamicTimeline._views
_views
Definition: dynamic_timeline.py:110
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_stop
def navigate_stop(self)
Definition: dynamic_timeline.py:928
rqt_py_trees.dynamic_timeline.DynamicTimeline._play_all
_play_all
Definition: dynamic_timeline.py:87
rqt_py_trees.dynamic_timeline.DynamicTimeline.reset_zoom
def reset_zoom(self)
Definition: dynamic_timeline.py:643
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_rewind
def navigate_rewind(self)
Definition: dynamic_timeline.py:942
rqt_py_trees.dynamic_timeline.DynamicTimeline.handle_close
def handle_close(self)
Definition: dynamic_timeline.py:142
rqt_py_trees.dynamic_timeline.DynamicTimeline._listeners
_listeners
Definition: dynamic_timeline.py:111
rqt_py_trees.dynamic_timeline.DynamicTimeline._stop_redraw_timer
def _stop_redraw_timer(self)
Definition: dynamic_timeline.py:137
rqt_py_trees.dynamic_timeline_frame.DynamicTimelineFrame
Definition: dynamic_timeline_frame.py:68
rqt_py_trees.dynamic_timeline.DynamicTimeline.background_progress
background_progress
Definition: dynamic_timeline.py:121
rqt_py_trees.dynamic_timeline.DynamicTimeline.resume
def resume(self)
Definition: dynamic_timeline.py:503
rqt_py_trees.dynamic_timeline.DynamicTimeline.start_publishing
def start_publishing(self, topic)
Definition: dynamic_timeline.py:656
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_entries_with_bags
def get_entries_with_bags(self, topic, start_stamp, end_stamp)
Definition: dynamic_timeline.py:359
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_play
def navigate_play(self)
Definition: dynamic_timeline.py:922
rqt_py_trees.dynamic_timeline.DynamicTimeline.toggle_recording
def toggle_recording(self)
Definition: dynamic_timeline.py:823
rqt_py_trees.dynamic_timeline.DynamicTimeline._step_playhead
def _step_playhead(self)
Definition: dynamic_timeline.py:714
rqt_py_trees.dynamic_timeline.DynamicTimeline._playhead_positions_cvs
_playhead_positions_cvs
Definition: dynamic_timeline.py:88
rqt_py_trees.dynamic_timeline.DynamicTimeline.timeline_updated
timeline_updated
Definition: dynamic_timeline.py:63
rqt_py_trees.dynamic_timeline.DynamicTimeline.zoom_out
def zoom_out(self)
Definition: dynamic_timeline.py:640
rqt_py_trees.dynamic_timeline.DynamicTimeline.has_listeners
def has_listeners(self, topic)
Definition: dynamic_timeline.py:863
rqt_bag::message_listener_thread
rqt_py_trees.dynamic_timeline.DynamicTimeline.stick_to_end
stick_to_end
Definition: dynamic_timeline.py:101
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_entries
def get_entries(self, topics, start_stamp, end_stamp)
Definition: dynamic_timeline.py:317
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_previous
def navigate_previous(self)
Definition: dynamic_timeline.py:932
rqt_py_trees.dynamic_timeline.DynamicTimeline._get_topics
def _get_topics(self)
Definition: dynamic_timeline.py:282
rqt_py_trees.dynamic_timeline.DynamicTimeline._play_speed
_play_speed
Definition: dynamic_timeline.py:86
rqt_py_trees.dynamic_timeline.DynamicTimeline._topics
_topics
Definition: dynamic_timeline.py:74
rqt_py_trees.dynamic_timeline.DynamicTimeline.get_datatype
def get_datatype(self, topic)
Definition: dynamic_timeline.py:299
rqt_bag::timeline_menu
rqt_py_trees.dynamic_timeline.DynamicTimeline.__init__
def __init__(self, context, publish_clock)
Definition: dynamic_timeline.py:67
rqt_py_trees.dynamic_timeline.DynamicTimeline.navigate_end
def navigate_end(self)
Definition: dynamic_timeline.py:965


rqt_py_trees
Author(s): Thibault Kruse, Michal Staniaszek, Daniel Stonier, Naveed Usmani
autogenerated on Wed Mar 2 2022 00:59:03