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


rqt_py_trees
Author(s): Thibault Kruse, Michal Staniaszek, Daniel Stonier, Naveed Usmani
autogenerated on Mon Jun 10 2019 14:55:56