From c53e99ee416c8f749e229896e20f28beedc219e5 Mon Sep 17 00:00:00 2001
From: Jan NIJS <dr.oblivium@gmail.com>
Date: Sun, 22 Jan 2017 14:21:38 +0100
Subject: [PATCH] LP-597 Progress bar for GCS log replay - add progress bar and
 a stop button

- Added stop button:
  - start: starts from 0 position after stop signal, resumes from current position after pause signal.
  - pause: freezes at current position, also freezes scopes.
  - stop: stops replay and resets start position to 0 (does not close the logfile)
- Creates an index of the timestamp positions in the logfile for quick seeking to the target position in the log.
- Start, End and current position timestamps are visible in the GUI below the position slider.
- Determine replay position by moving the position slider
- Update position label while changing position bar
- Speed widget: lowest multiplier is now 0.1 instead of 0.
  Only set one decimal of precision.
  More decimals seem useless at this time.
---
 ground/gcs/src/libs/utils/logfile.cpp         | 359 +++++++++++++++++-
 ground/gcs/src/libs/utils/logfile.h           |  30 +-
 ground/gcs/src/plugins/logging/logging.ui     | 145 ++++++-
 .../plugins/logging/logginggadgetwidget.cpp   | 188 +++++++++
 .../src/plugins/logging/logginggadgetwidget.h |  22 +-
 5 files changed, 714 insertions(+), 30 deletions(-)

diff --git a/ground/gcs/src/libs/utils/logfile.cpp b/ground/gcs/src/libs/utils/logfile.cpp
index 95b400d68..e1e0ece21 100644
--- a/ground/gcs/src/libs/utils/logfile.cpp
+++ b/ground/gcs/src/libs/utils/logfile.cpp
@@ -26,6 +26,8 @@
 
 #include <QDebug>
 #include <QtGlobal>
+#include <QDataStream>
+#include <QThread> // DEBUG: to display the thread ID
 
 LogFile::LogFile(QObject *parent) : QIODevice(parent),
     m_timer(this),
@@ -34,9 +36,12 @@ LogFile::LogFile(QObject *parent) : QIODevice(parent),
     m_lastPlayed(0),
     m_timeOffset(0),
     m_playbackSpeed(1.0),
-    paused(false),
+    m_replayStatus(STOPPED),
     m_useProvidedTimeStamp(false),
-    m_providedTimeStamp(0)
+    m_providedTimeStamp(0),
+    m_beginTimeStamp(0),
+    m_endTimeStamp(0),
+    m_timer_tick(0)
 {
     connect(&m_timer, &QTimer::timeout, this, &LogFile::timerFired);
 }
@@ -137,14 +142,58 @@ qint64 LogFile::bytesAvailable() const
     return len;
 }
 
+/**
+    timerFired()
+
+    This function is called at a 10 ms interval to fill the replay buffers.
+
+ */
+
 void LogFile::timerFired()
 {
+    if (m_replayStatus != PLAYING) {
+        return;
+    }
+
+    m_timer_tick++;
+    if ( m_timer_tick % 100 == 0 ) {
+      qDebug() << "----------------------------------------------------------";
+      qDebug() << "LogFile::timerFired() -> Tick = " << m_timer_tick;
+    }
+
     if (m_file.bytesAvailable() > 4) {
         int time;
         time = m_myTime.elapsed();
 
-        // TODO: going back in time will be a problem
-        while ((m_lastPlayed + ((double)(time - m_timeOffset) * m_playbackSpeed) > m_nextTimeStamp)) {
+        /*
+            This code generates an advancing window. All samples that fit in the window
+            are replayed. The window is about the size of the timer interval: 10 ms.
+  
+            Description of used variables:
+
+            time              : time passed since start of playback (in ms) - current
+            m_timeOffset      : time passed since start of playback (in ms) - when timerFired() was previously run
+            m_lastPlayed      : next log timestamp to advance to (in ms)
+            m_nextTimeStamp   : timestamp of most recently read log entry (in ms)
+            m_playbackSpeed   : 1 .. 10 replay speedup factor
+
+         */
+
+        while ( m_nextTimeStamp < (m_lastPlayed + (double)(time - m_timeOffset) * m_playbackSpeed) ) {
+//            if ( m_timer_tick % 100 == 0 ) {
+//            if ( true ) {
+//              qDebug() << "LogFile::timerFired() -> m_lastPlayed    = " << m_lastPlayed;
+//              qDebug() << "LogFile::timerFired() -> m_nextTimeStamp = " << m_nextTimeStamp;
+//              qDebug() << "LogFile::timerFired() -> time            = " << time;
+//              qDebug() << "LogFile::timerFired() -> m_timeOffset    = " << m_timeOffset;
+//              qDebug() << "---";
+//              qDebug() << "LogFile::timerFired() -> m_nextTimeStamp = " << m_nextTimeStamp;
+//              qDebug() << "LogFile::timerFired() -> (m_lastPlayed + (double)(time - m_timeOffset) * m_playbackSpeed) = " << (m_lastPlayed + (double)(time - m_timeOffset) * m_playbackSpeed);
+//              qDebug() << "---";
+//            }
+
+            // advance the replay window for the next time period
+
             m_lastPlayed += ((double)(time - m_timeOffset) * m_playbackSpeed);
 
             // read data size
@@ -178,6 +227,10 @@ void LogFile::timerFired()
 
             emit readyRead();
 
+            // rate-limit slider bar position updates to 10 updates per second
+            if (m_timer_tick % 10 == 0) {
+                emit replayPosition(m_nextTimeStamp);
+            }
             // read next timestamp
             if (m_file.bytesAvailable() < (qint64)sizeof(m_nextTimeStamp)) {
                 qDebug() << "LogFile - end of log file reached";
@@ -196,7 +249,7 @@ void LogFile::timerFired()
             }
 
             m_timeOffset = time;
-            time = m_myTime.elapsed();
+            time = m_myTime.elapsed(); // number of milliseconds since start of playback
         }
     } else {
         qDebug() << "LogFile - end of log file reached";
@@ -209,8 +262,28 @@ bool LogFile::isPlaying() const
     return m_file.isOpen() && m_timer.isActive();
 }
 
+/**
+ * FUNCTION: startReplay()
+ *
+ * Starts replaying a newly opened logfile.
+ * Starts a timer: m_timer
+ *
+ * This function and the stopReplay() function should only ever be called from the same thread.
+ * This is required for correctly controlling the timer.
+ *
+ */
 bool LogFile::startReplay()
 {
+    qDebug() << "startReplay(): start of function, current Thread ID is: " << QThread::currentThreadId();
+
+    // Walk through logfile and create timestamp index
+    // Don't start replay if there was a problem indexing the logfile.
+    if (!buildIndex()) {
+        return false;
+    }
+
+    m_timer_tick = 0;
+
     if (!m_file.isOpen() || m_timer.isActive()) {
         return false;
     }
@@ -221,7 +294,9 @@ bool LogFile::startReplay()
     m_lastPlayed        = 0;
     m_previousTimeStamp = 0;
     m_nextTimeStamp     = 0;
+    m_mutex.lock();
     m_dataBuffer.clear();
+    m_mutex.unlock();
 
     // read next timestamp
     if (m_file.bytesAvailable() < (qint64)sizeof(m_nextTimeStamp)) {
@@ -232,50 +307,312 @@ bool LogFile::startReplay()
 
     m_timer.setInterval(10);
     m_timer.start();
-    paused = false;
+    m_replayStatus = PLAYING;
 
     emit replayStarted();
     return true;
 }
 
+/**
+ * FUNCTION: stopReplay()
+ *
+ * Stops replaying the logfile.
+ * Stops the timer: m_timer
+ *
+ * This function and the startReplay() function should only ever be called from the same thread.
+ * This is a requirement to be able to control the timer.
+ *
+ */
 bool LogFile::stopReplay()
 {
-    if (!m_file.isOpen() || !(m_timer.isActive() || paused)) {
+    qDebug() << "stopReplay(): start of function, current Thread ID is: " << QThread::currentThreadId();
+
+    if (!m_file.isOpen() || !m_timer.isActive()) {
         return false;
     }
     qDebug() << "LogFile - stopReplay";
     m_timer.stop();
-    paused = false;
+    m_replayStatus = STOPPED;
 
     emit replayFinished();
     return true;
 }
 
+
+/**
+ * SLOT: restartReplay()
+ *
+ * This function starts replay from the begining of the currently opened logfile.
+ *
+ */
+void LogFile::restartReplay()
+{
+    qDebug() << "restartReplay(): start of function, current Thread ID is: " << QThread::currentThreadId();
+
+    resumeReplayFrom(0);
+
+    qDebug() << "restartReplay(): end of function, current Thread ID is: " << QThread::currentThreadId();
+}
+
+/**
+ * SLOT: haltReplay()
+ *
+ * Stops replay without storing the current playback position
+ *
+ */
+void LogFile::haltReplay()
+{
+    qDebug() << "haltReplay(): start of function, current Thread ID is: " << QThread::currentThreadId();
+
+    qDebug() << "haltReplay() time = m_myTime.elapsed() = " << m_myTime.elapsed();
+    qDebug() << "haltReplay() m_timeOffset = " << m_timeOffset;
+    qDebug() << "haltReplay() m_nextTimeStamp = " << m_nextTimeStamp;
+    qDebug() << "haltReplay() m_lastPlayed = " << m_lastPlayed;
+
+    m_replayStatus = STOPPED;
+
+    qDebug() << "haltReplay(): end of function, current Thread ID is: " << QThread::currentThreadId();
+}
+/**
+ * SLOT: pauseReplay()
+ *
+ * Pauses replay while storing the current playback position
+ *
+ */
 bool LogFile::pauseReplay()
 {
+    qDebug() << "pauseReplay(): start of function, current Thread ID is: " << QThread::currentThreadId();
+
+    qDebug() << "pauseReplay() time = m_myTime.elapsed() = " << m_myTime.elapsed();
+    qDebug() << "pauseReplay() m_timeOffset = " << m_timeOffset;
+    qDebug() << "pauseReplay() m_nextTimeStamp = " << m_nextTimeStamp;
+    qDebug() << "pauseReplay() m_lastPlayed = " << m_lastPlayed;
+
     if (!m_timer.isActive()) {
         return false;
     }
     qDebug() << "LogFile - pauseReplay";
     m_timer.stop();
-    paused = true;
+    m_replayStatus = PAUSED;
 
     // hack to notify UI that replay paused
     emit replayStarted();
     return true;
 }
 
+/**
+ * SLOT: resumeReplay()
+ *
+ * Resumes replay from the stored playback position
+ *
+ */
 bool LogFile::resumeReplay()
 {
+    qDebug() << "resumeReplay(): start of function, current Thread ID is: " << QThread::currentThreadId();
+
+    m_mutex.lock();
+    m_dataBuffer.clear();
+    m_mutex.unlock();
+
+    m_file.seek(0);
+
+    for (int i = 0; i < m_timeStamps.size(); ++i) {
+        if (m_timeStamps.at(i) >= m_lastPlayed) {
+            m_file.seek(m_timeStampPositions.at(i));
+            break;
+        }
+    }
+    m_file.read((char *)&m_nextTimeStamp, sizeof(m_nextTimeStamp));
+
+    m_myTime.restart();
+    m_myTime = m_myTime.addMSecs(-m_timeOffset); // Set startpoint this far back in time.
+
+    qDebug() << "resumeReplay() time = m_myTime.elapsed() = " << m_myTime.elapsed();
+    qDebug() << "resumeReplay() m_timeOffset = " << m_timeOffset;
+    qDebug() << "resumeReplay() m_nextTimeStamp = " << m_nextTimeStamp;
+    qDebug() << "resumeReplay() m_lastPlayed = " << m_lastPlayed;
+
+    qDebug() << "resumeReplay(): end of function, current Thread ID is: " << QThread::currentThreadId();
     if (m_timer.isActive()) {
         return false;
     }
     qDebug() << "LogFile - resumeReplay";
     m_timeOffset = m_myTime.elapsed();
     m_timer.start();
-    paused = false;
+    m_replayStatus = PLAYING;
 
-    // hack to notify UI that replay resumed
+    // Notify UI that replay has been resumed
     emit replayStarted();
     return true;
 }
+
+/**
+ * SLOT: resumeReplayFrom()
+ *
+ * Resumes replay from the given position
+ *
+ */
+void LogFile::resumeReplayFrom(quint32 desiredPosition)
+{
+    qDebug() << "resumeReplayFrom(): start of function, current Thread ID is: " << QThread::currentThreadId();
+
+    m_mutex.lock();
+    m_dataBuffer.clear();
+    m_mutex.unlock();
+
+    m_file.seek(0);
+
+    qint32 i;
+    for (i = 0; i < m_timeStamps.size(); ++i) {
+        if (m_timeStamps.at(i) >= desiredPosition) {
+            m_file.seek(m_timeStampPositions.at(i));
+            m_lastPlayed = m_timeStamps.at(i);
+            break;
+        }
+    }
+    m_file.read((char *)&m_nextTimeStamp, sizeof(m_nextTimeStamp));
+
+    if (m_nextTimeStamp != m_timeStamps.at(i)) {
+        qDebug() << "resumeReplayFrom() m_nextTimeStamp != m_timeStamps.at(i) -> " << m_nextTimeStamp << " != " << m_timeStamps.at(i);
+    }
+
+//    m_timeOffset = (m_lastPlayed - m_nextTimeStamp) / m_playbackSpeed;
+    m_timeOffset = 0;
+
+    m_myTime.restart();
+//    m_myTime = m_myTime.addMSecs(-m_timeOffset); // Set startpoint this far back in time.
+    // TODO: The above line is a possible memory leak. I'm not sure how to handle this correctly.
+
+    qDebug() << "resumeReplayFrom() time = m_myTime.elapsed() = " << m_myTime.elapsed();
+    qDebug() << "resumeReplayFrom() m_timeOffset = " << m_timeOffset;
+    qDebug() << "resumeReplayFrom() m_nextTimeStamp = " << m_nextTimeStamp;
+    qDebug() << "resumeReplayFrom() m_lastPlayed = " << m_lastPlayed;
+
+    m_replayStatus = PLAYING;
+    emit replayStarted();
+
+    qDebug() << "resumeReplayFrom(): end of function, current Thread ID is: " << QThread::currentThreadId();
+}
+
+/**
+ * FUNCTION: getReplayStatus()
+ *
+ * Returns the current replay status.
+ *
+ */
+ReplayState LogFile::getReplayStatus()
+{
+    return m_replayStatus;
+}
+
+/**
+ * FUNCTION: buildIndex()
+ *
+ * Walk through the opened logfile and sets the start and end position timestamps
+ * Also builds an index for quickly skipping to a specific position in the logfile.
+ *
+ * returns true when indexing has completed successfully
+ * returns false when a problem was encountered
+ *
+ */
+bool LogFile::buildIndex()
+{
+    quint32 timeStamp;
+    qint64 totalSize;
+    qint64 readPointer = 0;
+    quint64 index = 0;
+
+    QByteArray arr     = m_file.readAll();
+
+    totalSize = arr.size();
+    QDataStream dataStream(&arr, QIODevice::ReadOnly);
+
+    // set the start timestamp
+    if (totalSize - readPointer >= 4) {
+        dataStream.readRawData((char *)&timeStamp, 4);
+        m_timeStamps.append(timeStamp);
+        m_timeStampPositions.append(readPointer);
+        qDebug() << "LogFile::buildIndex() element index = " << index << " \t-> timestamp = " << timeStamp << " \t-> bytes in file = " << readPointer;
+        readPointer += 4;
+        index++;
+        m_beginTimeStamp = timeStamp;
+        m_endTimeStamp = timeStamp;
+    }
+
+    while (true) {
+        qint64 dataSize;
+
+        // Check if there are enough bytes remaining for a correct "dataSize" field
+        if (totalSize - readPointer < (qint64)sizeof(dataSize)) {
+            qDebug() << "Error: Logfile corrupted! Unexpected end of file";
+            return false;
+        }
+
+        // Read the dataSize field
+        dataStream.readRawData((char *)&dataSize, sizeof(dataSize));
+        readPointer += sizeof(dataSize);
+
+        if (dataSize < 1 || dataSize > (1024 * 1024)) {
+            qDebug() << "Error: Logfile corrupted! Unlikely packet size: " << dataSize << "\n";
+            return false;
+        }
+
+        // Check if there are enough bytes remaining
+        if (totalSize - readPointer < dataSize) {
+            qDebug() << "Error: Logfile corrupted! Unexpected end of file";
+            return false;
+        }
+
+        // skip reading the data (we don't need it at this point)
+        readPointer += dataStream.skipRawData(dataSize);
+
+        // read the next timestamp
+        if (totalSize - readPointer >= 4) {
+            dataStream.readRawData((char *)&timeStamp, 4);
+            qDebug() << "LogFile::buildIndex() element index = " << index << " \t-> timestamp = " << timeStamp << " \t-> bytes in file = " << readPointer;
+
+            // some validity checks
+            if (timeStamp < m_endTimeStamp // logfile goes back in time
+                || (timeStamp - m_endTimeStamp) > (60 * 60 * 1000)) { // gap of more than 60 minutes)
+                qDebug() << "Error: Logfile corrupted! Unlikely timestamp " << timeStamp << " after " << m_endTimeStamp;
+//                return false;
+            }
+
+            m_timeStamps.append(timeStamp);
+            m_timeStampPositions.append(readPointer);
+            readPointer += 4;
+            index++;
+            m_endTimeStamp = timeStamp;
+        } else {
+            // Break without error (we expect to end at this location when we are at the end of the logfile)
+            break;
+        }
+    }
+
+    qDebug() << "buildIndex() -> first timestamp in log = " << m_beginTimeStamp;
+    qDebug() << "buildIndex() -> last timestamp in log = " << m_endTimeStamp;
+
+    emit updateBeginAndEndtimes(m_beginTimeStamp, m_endTimeStamp);
+
+    // reset the read pointer to the start of the file
+    m_file.seek(0);
+
+    return true;
+}
+
+/**
+ * FUNCTION: setReplaySpeed()
+ *
+ * Update the replay speed.
+ *
+ * FIXME: currently, changing the replay speed, while skipping through the logfile
+ * with the position bar causes position alignment to be lost.
+ * 
+ */
+void LogFile::setReplaySpeed(double val)
+{
+    m_playbackSpeed = val;
+    qDebug() << "Playback speed is now " << QString("%1").arg(m_playbackSpeed, 4, 'f', 2, QChar('0'));
+}
+
+
diff --git a/ground/gcs/src/libs/utils/logfile.h b/ground/gcs/src/libs/utils/logfile.h
index 3c3616e20..49530926e 100644
--- a/ground/gcs/src/libs/utils/logfile.h
+++ b/ground/gcs/src/libs/utils/logfile.h
@@ -34,6 +34,9 @@
 #include <QDebug>
 #include <QBuffer>
 #include <QFile>
+#include <QVector>
+
+typedef enum { PLAYING, PAUSED, STOPPED } ReplayState;
 
 class QTCREATOR_UTILS_EXPORT LogFile : public QIODevice {
     Q_OBJECT
@@ -75,32 +78,34 @@ public:
         m_providedTimeStamp = providedTimestamp;
     }
 
+    ReplayState getReplayStatus();
+
 public slots:
-    void setReplaySpeed(double val)
-    {
-        m_playbackSpeed = val;
-        qDebug() << "Playback speed is now" << m_playbackSpeed;
-    };
+    void setReplaySpeed(double val);
     bool startReplay();
     bool stopReplay();
     bool pauseReplay();
     bool resumeReplay();
+    void resumeReplayFrom(quint32);
+    void restartReplay();
+    void haltReplay();
 
 protected slots:
     void timerFired();
 
 signals:
-    void readReady();
     void replayStarted();
     void replayFinished();
+    void replayPosition(quint32);
+    void updateBeginAndEndtimes(quint32, quint32);
 
 protected:
     QByteArray m_dataBuffer;
     QTimer m_timer;
     QTime m_myTime;
     QFile m_file;
-    qint32 m_previousTimeStamp;
-    qint32 m_nextTimeStamp;
+    quint32 m_previousTimeStamp;
+    quint32 m_nextTimeStamp;
     double m_lastPlayed;
     // QMutex wants to be mutable
     // http://stackoverflow.com/questions/25521570/can-mutex-locking-function-be-marked-as-const
@@ -108,11 +113,18 @@ protected:
 
     int m_timeOffset;
     double m_playbackSpeed;
-    bool paused;
+    ReplayState m_replayStatus;
 
 private:
     bool m_useProvidedTimeStamp;
     qint32 m_providedTimeStamp;
+    quint32 m_beginTimeStamp;
+    quint32 m_endTimeStamp;
+    quint32 m_timer_tick;
+    QVector<quint32> m_timeStamps;
+    QVector<qint64> m_timeStampPositions;
+
+    bool buildIndex();
 };
 
 #endif // LOGFILE_H
diff --git a/ground/gcs/src/plugins/logging/logging.ui b/ground/gcs/src/plugins/logging/logging.ui
index 05bad61aa..81a3e64db 100644
--- a/ground/gcs/src/plugins/logging/logging.ui
+++ b/ground/gcs/src/plugins/logging/logging.ui
@@ -7,19 +7,19 @@
     <x>0</x>
     <y>0</y>
     <width>439</width>
-    <height>122</height>
+    <height>150</height>
    </rect>
   </property>
   <property name="sizePolicy">
    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-    <horstretch>100</horstretch>
-    <verstretch>80</verstretch>
+    <horstretch>1</horstretch>
+    <verstretch>1</verstretch>
    </sizepolicy>
   </property>
   <property name="minimumSize">
    <size>
     <width>100</width>
-    <height>80</height>
+    <height>150</height>
    </size>
   </property>
   <property name="windowTitle">
@@ -27,9 +27,9 @@
   </property>
   <layout class="QVBoxLayout" name="verticalLayout_2">
    <item>
-    <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
+    <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
      <item>
-      <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0">
+      <layout class="QHBoxLayout" name="horizontalLayout" stretch="2,2,2,0,0,0">
        <property name="sizeConstraint">
         <enum>QLayout::SetNoConstraint</enum>
        </property>
@@ -71,7 +71,7 @@
           </size>
          </property>
          <property name="text">
-          <string notr="true">Pause</string>
+          <string>Pause</string>
          </property>
          <property name="icon">
           <iconset resource="../notify/res.qrc">
@@ -79,6 +79,29 @@
          </property>
         </widget>
        </item>
+       <item>
+        <widget class="QPushButton" name="stopButton">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="minimumSize">
+          <size>
+           <width>30</width>
+           <height>0</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Stop</string>
+         </property>
+         <property name="icon">
+          <iconset resource="../notify/res.qrc">
+           <normaloff>:/notify/images/delete.png</normaloff>:/notify/images/delete.png</iconset>
+         </property>
+        </widget>
+       </item>
        <item>
         <spacer name="horizontalSpacer_2">
          <property name="orientation">
@@ -125,11 +148,17 @@
        </item>
        <item>
         <widget class="QDoubleSpinBox" name="playbackSpeed">
+         <property name="decimals">
+          <number>1</number>
+         </property>
+         <property name="minimum">
+          <double>0.10000000000000</double>
+         </property>
          <property name="maximum">
           <double>10.000000000000000</double>
          </property>
          <property name="singleStep">
-          <double>0.100000000000000</double>
+          <double>0.10000000000000</double>
          </property>
          <property name="value">
           <double>1.000000000000000</double>
@@ -151,6 +180,106 @@
        </item>
       </layout>
      </item>
+     <item>
+      <widget class="QSlider" name="playBackPosition">
+       <property name="tracking">
+        <bool>true</bool>
+       </property>
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="invertedAppearance">
+        <bool>false</bool>
+       </property>
+       <property name="invertedControls">
+        <bool>false</bool>
+       </property>
+       <property name="tickPosition">
+        <enum>QSlider::TicksBothSides</enum>
+       </property>
+       <property name="tickInterval">
+        <number>5</number>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout_8">
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QLabel" name="startTimeLabel">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="horizontalSpacer_3">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>40</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QLabel" name="positionTimestampLabel">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="horizontalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>40</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QLabel" name="endTimeLabel">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
     </layout>
    </item>
    <item>
diff --git a/ground/gcs/src/plugins/logging/logginggadgetwidget.cpp b/ground/gcs/src/plugins/logging/logginggadgetwidget.cpp
index 659a65b30..75a5fc2e2 100644
--- a/ground/gcs/src/plugins/logging/logginggadgetwidget.cpp
+++ b/ground/gcs/src/plugins/logging/logginggadgetwidget.cpp
@@ -40,6 +40,15 @@ LoggingGadgetWidget::LoggingGadgetWidget(QWidget *parent) : QWidget(parent), log
 
     ExtensionSystem::PluginManager *pm = ExtensionSystem::PluginManager::instance();
     scpPlugin = pm->getObject<ScopeGadgetFactory>();
+
+    disableButtons();
+
+    sliderActionDelay.setSingleShot(true);
+    sliderActionDelay.setInterval(200); // Delay for 200 ms
+
+    connect(&sliderActionDelay, SIGNAL(timeout()), this, SLOT(sendResumeReplayFrom()));
+
+    m_storedPosition = 0;
 }
 
 LoggingGadgetWidget::~LoggingGadgetWidget()
@@ -51,6 +60,31 @@ void LoggingGadgetWidget::setPlugin(LoggingPlugin *p)
 {
     loggingPlugin = p;
 
+    connect(p->getLogfile(), SIGNAL(updateBeginAndEndtimes(quint32, quint32)), this, SLOT(updateBeginAndEndtimes(quint32, quint32)));
+    connect(p->getLogfile(), SIGNAL(replayPosition(quint32)), this, SLOT(replayPosition(quint32)));
+    connect(m_logging->playBackPosition, SIGNAL(valueChanged(int)), this, SLOT(sliderMoved(int)));
+    connect(this, SIGNAL(resumeReplayFrom(quint32)), p->getLogfile(), SLOT(resumeReplayFrom(quint32)));
+
+    connect(this, SIGNAL(startReplay()), p->getLogfile(), SLOT(restartReplay()));
+    connect(this, SIGNAL(stopReplay()), p->getLogfile(), SLOT(haltReplay()));
+    connect(this, SIGNAL(pauseReplay()), p->getLogfile(), SLOT(pauseReplay()));
+    connect(this, SIGNAL(resumeReplay()), p->getLogfile(), SLOT(resumeReplay()));
+
+    connect(this, SIGNAL(startReplay()), scpPlugin, SLOT(startPlotting()));
+    connect(this, SIGNAL(stopReplay()), scpPlugin, SLOT(stopPlotting()));
+    connect(this, SIGNAL(pauseReplay()), scpPlugin, SLOT(stopPlotting()));
+    connect(this, SIGNAL(resumeReplay()), scpPlugin, SLOT(startPlotting()));
+
+    connect(m_logging->playButton, SIGNAL(clicked()), this, SLOT(playButton()));
+    connect(m_logging->pauseButton, SIGNAL(clicked()), this, SLOT(pauseButton()));
+    connect(m_logging->stopButton, SIGNAL(clicked()), this, SLOT(stopButton()));
+
+    connect(p->getLogfile(), SIGNAL(replayStarted()), this, SLOT(enableButtons()));
+    connect(p->getLogfile(), SIGNAL(replayFinished()), this, SLOT(disableButtons()));
+    connect(p->getLogfile(), SIGNAL(replayFinished()), scpPlugin, SLOT(stopPlotting()));
+
+    connect(m_logging->playbackSpeed, SIGNAL(valueChanged(double)), p->getLogfile(), SLOT(setReplaySpeed(double)));
+
     connect(m_logging->playButton, &QPushButton::clicked, scpPlugin, &ScopeGadgetFactory::startPlotting);
     connect(m_logging->pauseButton, &QPushButton::clicked, scpPlugin, &ScopeGadgetFactory::stopPlotting);
 
@@ -64,6 +98,46 @@ void LoggingGadgetWidget::setPlugin(LoggingPlugin *p)
     stateChanged(loggingPlugin->getState());
 }
 
+void LoggingGadgetWidget::playButton()
+{
+    ReplayState replayState = (loggingPlugin->getLogfile())->getReplayStatus();
+
+    if (replayState == STOPPED) {
+        emit startReplay();
+    } else if (replayState == PAUSED) {
+        emit resumeReplay();
+    }
+    m_logging->playButton->setEnabled(false);
+    m_logging->pauseButton->setEnabled(true);
+    m_logging->stopButton->setEnabled(true);
+}
+
+void LoggingGadgetWidget::pauseButton()
+{
+    ReplayState replayState = (loggingPlugin->getLogfile())->getReplayStatus();
+
+    if (replayState == PLAYING) {
+        emit pauseReplay();
+    }
+    m_logging->playButton->setEnabled(true);
+    m_logging->pauseButton->setEnabled(false);
+    m_logging->stopButton->setEnabled(true);
+}
+
+void LoggingGadgetWidget::stopButton()
+{
+    ReplayState replayState = (loggingPlugin->getLogfile())->getReplayStatus();
+
+    if (replayState != STOPPED) {
+        emit stopReplay();
+    }
+    m_logging->playButton->setEnabled(true);
+    m_logging->pauseButton->setEnabled(false);
+    m_logging->stopButton->setEnabled(false);
+
+    replayPosition(0);
+}
+
 void LoggingGadgetWidget::stateChanged(LoggingPlugin::State state)
 {
     QString status;
@@ -86,8 +160,122 @@ void LoggingGadgetWidget::stateChanged(LoggingPlugin::State state)
     bool playing = loggingPlugin->getLogfile()->isPlaying();
     m_logging->playButton->setEnabled(enabled && !playing);
     m_logging->pauseButton->setEnabled(enabled && playing);
+    m_logging->stopButton->setEnabled(enabled && playing);
 }
 
+void LoggingGadgetWidget::updateBeginAndEndtimes(quint32 startTimeStamp, quint32 endTimeStamp)
+{
+    int startSec, startMin, endSec, endMin;
+
+    startSec = (startTimeStamp / 1000) % 60;
+    startMin = startTimeStamp / (60 * 1000);
+
+    endSec = (endTimeStamp / 1000) % 60;
+    endMin = endTimeStamp / (60 * 1000);
+
+    // update start and end labels
+    m_logging->startTimeLabel->setText(QString("%1:%2").arg(startMin, 2, 10, QChar('0')).arg(startSec, 2, 10, QChar('0')));
+    m_logging->endTimeLabel->setText(QString("%1:%2").arg(endMin, 2, 10, QChar('0')).arg(endSec, 2, 10, QChar('0')));
+
+    // Update position bar
+    m_logging->playBackPosition->setMinimum(startTimeStamp);
+    m_logging->playBackPosition->setMaximum(endTimeStamp);
+
+    m_logging->playBackPosition->setSingleStep((endTimeStamp - startTimeStamp) / 100);
+    m_logging->playBackPosition->setPageStep((endTimeStamp - startTimeStamp) / 10);
+    m_logging->playBackPosition->setTickInterval((endTimeStamp - startTimeStamp) / 10);
+    m_logging->playBackPosition->setTickPosition(QSlider::TicksBothSides);
+}
+
+void LoggingGadgetWidget::replayPosition(quint32 positionTimeStamp)
+{
+    // Update position bar, but only if the user is not updating the slider position
+    if (!m_logging->playBackPosition->isSliderDown() && !sliderActionDelay.isActive()) {
+        // Block signals during slider position update:
+        m_logging->playBackPosition->blockSignals(true);
+        m_logging->playBackPosition->setValue(positionTimeStamp);
+        m_logging->playBackPosition->blockSignals(false);
+
+        // update current position label
+        updatePositionLabel(positionTimeStamp);
+    }
+}
+
+void LoggingGadgetWidget::enableButtons()
+{
+    ReplayState replayState = (loggingPlugin->getLogfile())->getReplayStatus();
+
+    switch (replayState)
+    {
+    case STOPPED:
+        m_logging->playButton->setEnabled(true);
+        m_logging->pauseButton->setEnabled(false);
+        m_logging->stopButton->setEnabled(false);
+        break;
+
+    case PLAYING:
+        m_logging->playButton->setEnabled(false);
+        m_logging->pauseButton->setEnabled(true);
+        m_logging->stopButton->setEnabled(true);
+        break;
+
+    case PAUSED:
+        m_logging->playButton->setEnabled(true);
+        m_logging->pauseButton->setEnabled(false);
+        m_logging->stopButton->setEnabled(true);
+        break;
+    }
+    m_logging->playBackPosition->setEnabled(true);
+}
+
+void LoggingGadgetWidget::disableButtons()
+{
+//    m_logging->startTimeLabel->setText(QString(""));
+//    m_logging->endTimeLabel->setText(QString(""));
+
+    m_logging->playButton->setEnabled(false);
+    m_logging->pauseButton->setEnabled(false);
+    m_logging->stopButton->setEnabled(false);
+
+    m_logging->playBackPosition->setEnabled(false);
+}
+
+void LoggingGadgetWidget::sliderMoved(int position)
+{
+    qDebug() << "sliderMoved(): start of function, stored position was: " << m_storedPosition;
+
+    m_storedPosition = position;
+    // pause
+    emit pauseButton();
+
+    updatePositionLabel(position);
+
+    // Start or restarts a time-out after which replay is resumed from the new position.
+    sliderActionDelay.start();
+
+    qDebug() << "sliderMoved(): end of function, stored position is now: " << m_storedPosition;
+}
+
+void LoggingGadgetWidget::updatePositionLabel(quint32 positionTimeStamp)
+{
+    // update position timestamp label
+    int sec = (positionTimeStamp / 1000) % 60;
+    int min = positionTimeStamp / (60 * 1000);
+    m_logging->positionTimestampLabel->setText(QString("%1:%2").arg(min, 2, 10, QChar('0')).arg(sec, 2, 10, QChar('0')));
+}
+
+void LoggingGadgetWidget::sendResumeReplayFrom()
+{
+    qDebug() << "sendResumeReplayFrom(): start of function, stored position is: " << m_storedPosition;
+
+    emit resumeReplayFrom(m_storedPosition);
+
+    emit resumeReplay();
+
+    qDebug() << "sendResumeReplayFrom(): end of function, stored position is: " << m_storedPosition;
+}
+
+
 /**
  * @}
  * @}
diff --git a/ground/gcs/src/plugins/logging/logginggadgetwidget.h b/ground/gcs/src/plugins/logging/logginggadgetwidget.h
index 60b268e21..28c900915 100644
--- a/ground/gcs/src/plugins/logging/logginggadgetwidget.h
+++ b/ground/gcs/src/plugins/logging/logginggadgetwidget.h
@@ -37,6 +37,7 @@
 #include <QWidget>
 
 class Ui_Logging;
+class QTimer;
 
 class LoggingGadgetWidget : public QWidget {
     Q_OBJECT
@@ -48,15 +49,32 @@ public:
 
 protected slots:
     void stateChanged(LoggingPlugin::State state);
+    void updateBeginAndEndtimes(quint32 startTimeStamp, quint32 endTimeStamp);
+    void replayPosition(quint32 positionTimeStamp);
+    void playButton();
+    void pauseButton();
+    void stopButton();
+    void enableButtons();
+    void disableButtons();
+    void sliderMoved(int);
+    void sendResumeReplayFrom();
 
 signals:
-    void pause();
-    void play();
+    void startReplay();
+    void stopReplay();
+    void pauseReplay();
+    void resumeReplay();
+    void resumeReplayFrom(quint32 positionTimeStamp);
 
 private:
     Ui_Logging *m_logging;
     LoggingPlugin *loggingPlugin;
     ScopeGadgetFactory *scpPlugin;
+    QTimer sliderActionDelay;
+    quint32 m_storedPosition;
+
+    void updatePositionLabel(quint32 positionTimeStamp);
+
 };
 
 #endif /* LoggingGADGETWIDGET_H_ */