1
0
mirror of https://bitbucket.org/librepilot/librepilot.git synced 2025-02-20 10:54:14 +01:00

Merged in Oblivium/librepilot/LP-597_Progress_bar_for_GCS_log_replay (pull request #511)

LP-597 Progress bar for GCS log replay

Approved-by: Jan NIJS <dr.oblivium@gmail.com>
Approved-by: Philippe Renon <philippe_renon@yahoo.fr>
Approved-by: Lalanne Laurent <f5soh@free.fr>
This commit is contained in:
Jan NIJS 2018-09-29 09:56:01 +00:00 committed by Lalanne Laurent
commit 2f075df628
10 changed files with 822 additions and 101 deletions

View File

@ -2,7 +2,7 @@
******************************************************************************
*
* @file logfile.cpp
* @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2017.
* @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2017-2018.
* The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
* @see The GNU Public License (GPL) Version 3
*
@ -26,6 +26,9 @@
#include <QDebug>
#include <QtGlobal>
#include <QDataStream>
#define TIMESTAMP_SIZE_BYTES 4
LogFile::LogFile(QObject *parent) : QIODevice(parent),
m_timer(this),
@ -34,9 +37,12 @@ LogFile::LogFile(QObject *parent) : QIODevice(parent),
m_lastPlayed(0),
m_timeOffset(0),
m_playbackSpeed(1.0),
paused(false),
m_replayState(STOPPED),
m_useProvidedTimeStamp(false),
m_providedTimeStamp(0)
m_providedTimeStamp(0),
m_beginTimeStamp(0),
m_endTimeStamp(0),
m_timerTick(0)
{
connect(&m_timer, &QTimer::timeout, this, &LogFile::timerFired);
}
@ -137,36 +143,61 @@ 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_file.bytesAvailable() > 4) {
if (m_replayState != PLAYING) {
return;
}
m_timerTick++;
if (m_file.bytesAvailable() > TIMESTAMP_SIZE_BYTES) {
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 playback window. All samples that fit the window
are replayed. The window is about the size of the timer interval: 10 ms.
Description of used variables:
time : real-time interval since start of playback (in ms) - now()
m_timeOffset : real-time interval since start of playback (in ms) - when timerFired() was previously run
m_nextTimeStamp : read log until this log timestamp has been reached (in ms)
m_lastPlayed : log referenced timestamp advanced to during previous cycle (in ms)
m_playbackSpeed : 0.1 .. 1.0 .. 10 replay speedup factor
*/
while (m_nextTimeStamp < (m_lastPlayed + (double)(time - m_timeOffset) * m_playbackSpeed)) {
// advance the replay window for the next time period
m_lastPlayed += ((double)(time - m_timeOffset) * m_playbackSpeed);
// read data size
qint64 dataSize;
if (m_file.bytesAvailable() < (qint64)sizeof(dataSize)) {
qDebug() << "LogFile - end of log file reached";
stopReplay();
qDebug() << "LogFile replay - end of log file reached";
resetReplay();
return;
}
m_file.read((char *)&dataSize, sizeof(dataSize));
// check size consistency
if (dataSize < 1 || dataSize > (1024 * 1024)) {
qWarning() << "LogFile - corrupted log file! Unlikely packet size:" << dataSize;
qWarning() << "LogFile replay - corrupted log file! Unlikely packet size:" << dataSize;
stopReplay();
return;
}
// read data
if (m_file.bytesAvailable() < dataSize) {
qDebug() << "LogFile - end of log file reached";
stopReplay();
qDebug() << "LogFile replay - end of log file reached";
resetReplay();
return;
}
QByteArray data = m_file.read(dataSize);
@ -178,10 +209,14 @@ void LogFile::timerFired()
emit readyRead();
// rate-limit slider bar position updates to 10 updates per second
if (m_timerTick % 10 == 0) {
emit playbackPositionChanged(m_nextTimeStamp);
}
// read next timestamp
if (m_file.bytesAvailable() < (qint64)sizeof(m_nextTimeStamp)) {
qDebug() << "LogFile - end of log file reached";
stopReplay();
qDebug() << "LogFile replay - end of log file reached";
resetReplay();
return;
}
m_previousTimeStamp = m_nextTimeStamp;
@ -190,17 +225,17 @@ void LogFile::timerFired()
// some validity checks
if ((m_nextTimeStamp < m_previousTimeStamp) // logfile goes back in time
|| ((m_nextTimeStamp - m_previousTimeStamp) > 60 * 60 * 1000)) { // gap of more than 60 minutes
qWarning() << "LogFile - corrupted log file! Unlikely timestamp:" << m_nextTimeStamp << "after" << m_previousTimeStamp;
qWarning() << "LogFile replay - corrupted log file! Unlikely timestamp:" << m_nextTimeStamp << "after" << m_previousTimeStamp;
stopReplay();
return;
}
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";
stopReplay();
qDebug() << "LogFile replay - end of log file reached";
resetReplay();
}
}
@ -209,8 +244,26 @@ 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 correct control of the timer.
*
*/
bool LogFile::startReplay()
{
// Walk through logfile and create timestamp index
// Don't start replay if there was a problem indexing the logfile.
if (!buildIndex()) {
return false;
}
m_timerTick = 0;
if (!m_file.isOpen() || m_timer.isActive()) {
return false;
}
@ -221,7 +274,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,25 +287,125 @@ bool LogFile::startReplay()
m_timer.setInterval(10);
m_timer.start();
paused = false;
m_replayState = 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)) {
if (!m_file.isOpen()) {
return false;
}
if (m_timer.isActive()) {
m_timer.stop();
}
qDebug() << "LogFile - stopReplay";
m_timer.stop();
paused = false;
m_replayState = STOPPED;
emit replayFinished();
return true;
}
/**
* FUNCTION: resetReplay()
*
* Stops replaying the logfile.
* Stops the timer: m_timer
* Resets playback position to the start of the logfile
* through the emission of a replayCompleted signal.
*
*/
bool LogFile::resetReplay()
{
if (!m_file.isOpen()) {
return false;
}
if (m_timer.isActive()) {
m_timer.stop();
}
qDebug() << "LogFile - resetReplay";
m_replayState = STOPPED;
emit replayCompleted();
return true;
}
/**
* SLOT: resumeReplay()
*
* Resumes replay from the given position.
* If no position is given, resumes from the last position
*
*/
bool LogFile::resumeReplay(quint32 desiredPosition)
{
if (m_timer.isActive()) {
return false;
}
qDebug() << "LogFile - resumeReplay";
// Clear the playout buffer:
m_mutex.lock();
m_dataBuffer.clear();
m_mutex.unlock();
m_file.seek(0);
/* Skip through the logfile until we reach the desired position.
Looking for the next log timestamp after the desired position
has the advantage that it skips over parts of the log
where data might be missing.
*/
for (int i = 0; i < m_timeStamps.size(); i++) {
if (m_timeStamps.at(i) >= desiredPosition) {
int bytesToSkip = m_timeStampPositions.at(i);
bool seek_ok = m_file.seek(bytesToSkip);
if (!seek_ok) {
qWarning() << "LogFile resumeReplay - an error occurred while seeking through the logfile.";
}
m_lastPlayed = m_timeStamps.at(i);
break;
}
}
m_file.read((char *)&m_nextTimeStamp, sizeof(m_nextTimeStamp));
// Real-time timestamps don't not need to match the log timestamps.
// However the delta between real-time variables "m_timeOffset" and "m_myTime" is important.
// This delta determines the number of log entries replayed per cycle.
// Set the real-time interval to 0 to start with:
m_myTime.restart();
m_timeOffset = 0;
m_replayState = PLAYING;
m_timer.start();
// Notify UI that playback has resumed
emit replayStarted();
return true;
}
/**
* SLOT: pauseReplay()
*
* Pauses replay while storing the current playback position
*
*/
bool LogFile::pauseReplay()
{
if (!m_timer.isActive()) {
@ -258,24 +413,149 @@ bool LogFile::pauseReplay()
}
qDebug() << "LogFile - pauseReplay";
m_timer.stop();
paused = true;
// hack to notify UI that replay paused
emit replayStarted();
m_replayState = PAUSED;
return true;
}
bool LogFile::resumeReplay()
/**
* SLOT: pauseReplayAndResetPosition()
*
* Pauses replay and resets the playback position to the start of the logfile
*
*/
bool LogFile::pauseReplayAndResetPosition()
{
if (m_timer.isActive()) {
if (!m_file.isOpen() || !m_timer.isActive()) {
return false;
}
qDebug() << "LogFile - resumeReplay";
m_timeOffset = m_myTime.elapsed();
m_timer.start();
paused = false;
qDebug() << "LogFile - pauseReplayAndResetPosition";
m_timer.stop();
m_replayState = STOPPED;
m_timeOffset = 0;
m_lastPlayed = m_timeStamps.at(0);
m_previousTimeStamp = 0;
m_nextTimeStamp = 0;
return true;
}
/**
* FUNCTION: getReplayState()
*
* Returns the current replay status.
*
*/
ReplayState LogFile::getReplayState()
{
return m_replayState;
}
/**
* FUNCTION: buildIndex()
*
* Walk through the opened logfile and stores the first and last 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;
int bytesRead = 0;
qDebug() << "LogFile - buildIndex";
// Ensure empty vectors:
m_timeStampPositions.clear();
m_timeStamps.clear();
QByteArray arr = m_file.readAll();
totalSize = arr.size();
QDataStream dataStream(&arr, QIODevice::ReadOnly);
// set the first timestamp
if (totalSize - readPointer >= TIMESTAMP_SIZE_BYTES) {
bytesRead = dataStream.readRawData((char *)&timeStamp, TIMESTAMP_SIZE_BYTES);
if (bytesRead != TIMESTAMP_SIZE_BYTES) {
qWarning() << "LogFile buildIndex - read first timeStamp: readRawData returned unexpected number of bytes:" << bytesRead << "at position" << readPointer << "\n";
return false;
}
m_timeStamps.append(timeStamp);
m_timeStampPositions.append(readPointer);
readPointer += TIMESTAMP_SIZE_BYTES;
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)) {
qWarning() << "LogFile buildIndex - logfile corrupted! Unexpected end of file";
return false;
}
// Read the dataSize field and check for I/O errors
bytesRead = dataStream.readRawData((char *)&dataSize, sizeof(dataSize));
if (bytesRead != sizeof(dataSize)) {
qWarning() << "LogFile buildIndex - read dataSize: readRawData returned unexpected number of bytes:" << bytesRead << "at position" << readPointer << "\n";
return false;
}
readPointer += sizeof(dataSize);
if (dataSize < 1 || dataSize > (1024 * 1024)) {
qWarning() << "LogFile buildIndex - logfile corrupted! Unlikely packet size: " << dataSize << "\n";
return false;
}
// Check if there are enough bytes remaining
if (totalSize - readPointer < dataSize) {
qWarning() << "LogFile buildIndex - 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 >= TIMESTAMP_SIZE_BYTES) {
bytesRead = dataStream.readRawData((char *)&timeStamp, TIMESTAMP_SIZE_BYTES);
if (bytesRead != TIMESTAMP_SIZE_BYTES) {
qWarning() << "LogFile buildIndex - read timeStamp, readRawData returned unexpected number of bytes:" << bytesRead << "at position" << readPointer << "\n";
return false;
}
// some validity checks
if (timeStamp < m_endTimeStamp // logfile goes back in time
|| (timeStamp - m_endTimeStamp) > (60 * 60 * 1000)) { // gap of more than 60 minutes)
qWarning() << "LogFile buildIndex - logfile corrupted! Unlikely timestamp " << timeStamp << " after " << m_endTimeStamp;
return false;
}
m_timeStamps.append(timeStamp);
m_timeStampPositions.append(readPointer);
readPointer += TIMESTAMP_SIZE_BYTES;
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;
}
}
emit timesChanged(m_beginTimeStamp, m_endTimeStamp);
// reset the read pointer to the start of the file
m_file.seek(0);
// hack to notify UI that replay resumed
emit replayStarted();
return true;
}

View File

@ -2,7 +2,7 @@
******************************************************************************
*
* @file logfile.h
* @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2017.
* @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2017-2018.
* The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
* @see The GNU Public License (GPL) Version 3
*
@ -32,8 +32,10 @@
#include <QTimer>
#include <QMutexLocker>
#include <QDebug>
#include <QBuffer>
#include <QFile>
#include <QVector>
typedef enum { PLAYING, PAUSED, STOPPED } ReplayState;
class QTCREATOR_UTILS_EXPORT LogFile : public QIODevice {
Q_OBJECT
@ -75,6 +77,8 @@ public:
m_providedTimeStamp = providedTimestamp;
}
ReplayState getReplayState();
public slots:
void setReplaySpeed(double val)
{
@ -83,24 +87,28 @@ public slots:
};
bool startReplay();
bool stopReplay();
bool resumeReplay(quint32);
bool pauseReplay();
bool resumeReplay();
bool pauseReplayAndResetPosition();
protected slots:
void timerFired();
signals:
void readReady();
void replayStarted();
void replayFinished();
void replayFinished(); // Emitted on error during replay or when logfile disconnected
void replayCompleted(); // Emitted at the end of normal logfile playback
void playbackPositionChanged(quint32);
void timesChanged(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 +116,19 @@ protected:
int m_timeOffset;
double m_playbackSpeed;
bool paused;
ReplayState m_replayState;
private:
bool m_useProvidedTimeStamp;
qint32 m_providedTimeStamp;
quint32 m_beginTimeStamp;
quint32 m_endTimeStamp;
quint32 m_timerTick;
QVector<quint32> m_timeStamps;
QVector<qint64> m_timeStampPositions;
bool buildIndex();
bool resetReplay();
};
#endif // LOGFILE_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

View File

@ -24,3 +24,5 @@ OTHER_FILES += LoggingGadget.pluginspec
FORMS += logging.ui
RESOURCES += \
res.qrc

View File

@ -7,75 +7,156 @@
<x>0</x>
<y>0</y>
<width>439</width>
<height>122</height>
<height>120</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>118</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1">
<item>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,2,0,0,2,0,0">
<property name="spacing">
<number>4</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetNoConstraint</enum>
</property>
<item>
<widget class="QPushButton" name="playButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
<width>39</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Play</string>
<property name="maximumSize">
<size>
<width>39</width>
<height>30</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="icon">
<iconset resource="../notify/res.qrc">
<normaloff>:/notify/images/play.png</normaloff>:/notify/images/play.png</iconset>
<iconset resource="res.qrc">
<normaloff>:/logging/images/play.png</normaloff>:/logging/images/play.png</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pauseButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
<width>39</width>
<height>30</height>
</size>
</property>
<property name="text">
<string notr="true">Pause</string>
<property name="maximumSize">
<size>
<width>39</width>
<height>30</height>
</size>
</property>
<property name="icon">
<iconset resource="../notify/res.qrc">
<normaloff>:/notify/images/stop.png</normaloff>:/notify/images/stop.png</iconset>
<iconset resource="res.qrc">
<normaloff>:/logging/images/pause.png</normaloff>:/logging/images/pause.png</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>4</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>39</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>39</width>
<height>30</height>
</size>
</property>
<property name="icon">
<iconset resource="res.qrc">
<normaloff>:/logging/images/stop.png</normaloff>:/logging/images/stop.png</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
@ -92,6 +173,54 @@
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Playback speed:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="playbackSpeed">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.100000000000000</double>
</property>
<property name="maximum">
<double>10.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<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="label">
<property name="text">
@ -101,6 +230,12 @@
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="minimumSize">
<size>
<width>65</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<weight>75</weight>
@ -115,29 +250,50 @@
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<widget class="QSlider" name="playbackPosition">
<property name="tracking">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<widget class="QLabel" name="startTimeLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Playback speed:</string>
<string/>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="playbackSpeed">
<property name="maximum">
<double>10.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@ -149,6 +305,63 @@
</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="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</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="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@ -161,7 +374,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<height>5</height>
</size>
</property>
</spacer>
@ -169,7 +382,7 @@
</layout>
</widget>
<resources>
<include location="../notify/res.qrc"/>
<include location="res.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -1,13 +1,14 @@
/**
******************************************************************************
*
* @file GCSControlgadgetwidget.cpp
* @author The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
* @file logginggadgetwidget.cpp
* @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2018.
* The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
* @addtogroup GCSPlugins GCS Plugins
* @{
* @addtogroup GCSControlGadgetPlugin GCSControl Gadget Plugin
* @addtogroup LoggingGadgetPlugin Logging Gadget Plugin
* @{
* @brief A gadget to control the UAV, either from the keyboard or a joystick
* @brief A gadget to control playback of a GCS log.
*****************************************************************************/
/*
* This program is free software; you can redistribute it and/or modify
@ -40,6 +41,14 @@ LoggingGadgetWidget::LoggingGadgetWidget(QWidget *parent) : QWidget(parent), log
ExtensionSystem::PluginManager *pm = ExtensionSystem::PluginManager::instance();
scpPlugin = pm->getObject<ScopeGadgetFactory>();
disableWidgets();
// Configure timer to delay application of slider position action for 200ms
sliderActionDelay.setSingleShot(true);
sliderActionDelay.setInterval(200);
connect(&sliderActionDelay, SIGNAL(timeout()), this, SLOT(sliderAction()));
}
LoggingGadgetWidget::~LoggingGadgetWidget()
@ -50,20 +59,82 @@ LoggingGadgetWidget::~LoggingGadgetWidget()
void LoggingGadgetWidget::setPlugin(LoggingPlugin *p)
{
loggingPlugin = p;
connect(m_logging->playButton, &QPushButton::clicked, scpPlugin, &ScopeGadgetFactory::startPlotting);
connect(m_logging->pauseButton, &QPushButton::clicked, scpPlugin, &ScopeGadgetFactory::stopPlotting);
LogFile *logFile = loggingPlugin->getLogfile();
connect(m_logging->playButton, &QPushButton::clicked, logFile, &LogFile::resumeReplay);
connect(m_logging->pauseButton, &QPushButton::clicked, logFile, &LogFile::pauseReplay);
// GUI elements to gadgetwidget functions
connect(m_logging->playButton, &QPushButton::clicked, this, &LoggingGadgetWidget::playButtonAction);
connect(m_logging->pauseButton, &QPushButton::clicked, this, &LoggingGadgetWidget::pauseButtonAction);
connect(m_logging->stopButton, &QPushButton::clicked, this, &LoggingGadgetWidget::stopButtonAction);
connect(m_logging->playbackPosition, &QSlider::valueChanged, this, &LoggingGadgetWidget::sliderMoved);
connect(m_logging->playbackSpeed, static_cast<void(QDoubleSpinBox::*) (double)>(&QDoubleSpinBox::valueChanged), logFile, &LogFile::setReplaySpeed);
connect(loggingPlugin, &LoggingPlugin::stateChanged, this, &LoggingGadgetWidget::stateChanged);
// gadgetwidget functions to logfile actions
connect(this, &LoggingGadgetWidget::resumeReplay, logFile, &LogFile::resumeReplay);
connect(this, &LoggingGadgetWidget::pauseReplay, logFile, &LogFile::pauseReplay);
connect(this, &LoggingGadgetWidget::pauseReplayAndResetPosition, logFile, &LogFile::pauseReplayAndResetPosition);
// gadgetwidget functions to scope actions
connect(this, &LoggingGadgetWidget::resumeReplay, scpPlugin, &ScopeGadgetFactory::startPlotting);
connect(this, &LoggingGadgetWidget::pauseReplay, scpPlugin, &ScopeGadgetFactory::stopPlotting);
connect(this, &LoggingGadgetWidget::pauseReplayAndResetPosition, scpPlugin, &ScopeGadgetFactory::stopPlotting);
// Feedback from logfile to GUI
connect(loggingPlugin, &LoggingPlugin::stateChanged, this, &LoggingGadgetWidget::stateChanged);
connect(logFile, &LogFile::timesChanged, this, &LoggingGadgetWidget::setBeginAndEndTimes);
connect(logFile, &LogFile::playbackPositionChanged, this, &LoggingGadgetWidget::setPlaybackPosition);
connect(logFile, &LogFile::replayStarted, this, &LoggingGadgetWidget::enableWidgets);
connect(logFile, &LogFile::replayFinished, this, &LoggingGadgetWidget::disableWidgets);
connect(logFile, &LogFile::replayCompleted, this, &LoggingGadgetWidget::stopButtonAction);
// Feedback from logfile to scope
connect(logFile, &LogFile::replayFinished, scpPlugin, &ScopeGadgetFactory::stopPlotting);
// Perform actions as if the plugin state has been changed
stateChanged(loggingPlugin->getState());
}
void LoggingGadgetWidget::playButtonAction()
{
ReplayState replayState = loggingPlugin->getLogfile()->getReplayState();
if (replayState != PLAYING) {
emit resumeReplay(m_logging->playbackPosition->value());
}
m_logging->playButton->setVisible(false);
m_logging->pauseButton->setVisible(true);
m_logging->stopButton->setEnabled(true);
}
void LoggingGadgetWidget::pauseButtonAction()
{
ReplayState replayState = loggingPlugin->getLogfile()->getReplayState();
if (replayState == PLAYING) {
emit pauseReplay();
}
m_logging->playButton->setVisible(true);
m_logging->pauseButton->setVisible(false);
m_logging->stopButton->setEnabled(true);
m_logging->statusLabel->setText(tr("Paused"));
}
void LoggingGadgetWidget::stopButtonAction()
{
emit pauseReplayAndResetPosition();
m_logging->playButton->setVisible(true);
m_logging->pauseButton->setVisible(false);
m_logging->stopButton->setEnabled(false);
setPlaybackPosition(0);
m_logging->statusLabel->setText(tr("Stopped"));
}
void LoggingGadgetWidget::stateChanged(LoggingPlugin::State state)
{
QString status;
@ -71,7 +142,8 @@ void LoggingGadgetWidget::stateChanged(LoggingPlugin::State state)
switch (state) {
case LoggingPlugin::IDLE:
status = tr("Idle");
status = tr("Idle");
setPlaybackPosition(0);
break;
case LoggingPlugin::LOGGING:
status = tr("Logging");
@ -84,10 +156,126 @@ void LoggingGadgetWidget::stateChanged(LoggingPlugin::State state)
m_logging->statusLabel->setText(status);
bool playing = loggingPlugin->getLogfile()->isPlaying();
m_logging->playButton->setEnabled(enabled && !playing);
m_logging->pauseButton->setEnabled(enabled && playing);
m_logging->playButton->setEnabled(enabled);
m_logging->pauseButton->setEnabled(enabled);
m_logging->stopButton->setEnabled(enabled && playing);
if (playing) {
m_logging->playButton->setVisible(false);
m_logging->pauseButton->setVisible(true);
} else {
m_logging->playButton->setVisible(true);
m_logging->pauseButton->setVisible(false);
}
}
void LoggingGadgetWidget::setBeginAndEndTimes(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);
}
void LoggingGadgetWidget::setPlaybackPosition(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 position label
updatePositionLabel(positionTimeStamp);
}
}
void LoggingGadgetWidget::enableWidgets()
{
ReplayState replayState = loggingPlugin->getLogfile()->getReplayState();
m_logging->playButton->setEnabled(true);
m_logging->pauseButton->setEnabled(true);
switch (replayState) {
case STOPPED:
m_logging->stopButton->setEnabled(false);
m_logging->playButton->setVisible(true);
m_logging->pauseButton->setVisible(false);
break;
case PLAYING:
m_logging->stopButton->setEnabled(true);
m_logging->playButton->setVisible(false);
m_logging->pauseButton->setVisible(true);
break;
case PAUSED:
m_logging->stopButton->setEnabled(true);
m_logging->playButton->setVisible(true);
m_logging->pauseButton->setVisible(false);
break;
}
m_logging->playbackPosition->setEnabled(true);
}
void LoggingGadgetWidget::disableWidgets()
{
m_logging->playButton->setVisible(true);
m_logging->pauseButton->setVisible(false);
m_logging->playButton->setEnabled(false);
m_logging->pauseButton->setEnabled(false);
m_logging->stopButton->setEnabled(false);
m_logging->playbackPosition->setEnabled(false);
// reset start and end labels
m_logging->startTimeLabel->setText("");
m_logging->endTimeLabel->setText("");
}
void LoggingGadgetWidget::updatePositionLabel(quint32 positionTimeStamp)
{
// update position label -> MM:SS
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::sliderMoved(int position)
{
// pause playback while the user is dragging the slider to change position
emit pauseReplay();
updatePositionLabel(position);
// Start or restarts a time-out after which replay will resume from the new position.
sliderActionDelay.start();
}
void LoggingGadgetWidget::sliderAction()
{
emit resumeReplay(m_logging->playbackPosition->value());
}
/**
* @}
* @}

View File

@ -1,13 +1,14 @@
/**
******************************************************************************
*
* @file GCSControlgadgetwidget.h
* @author The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
* @file logginggadgetwidget.h
* @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2018.
* The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
* @addtogroup GCSPlugins GCS Plugins
* @{
* @addtogroup GCSControlGadgetPlugin GCSControl Gadget Plugin
* @addtogroup LoggingGadgetPlugin Logging Gadget Plugin
* @{
* @brief A place holder gadget plugin
* @brief A gadget to control playback of a GCS log.
*****************************************************************************/
/*
* This program is free software; you can redistribute it and/or modify
@ -37,6 +38,7 @@
#include <QWidget>
class Ui_Logging;
class QTimer;
class LoggingGadgetWidget : public QWidget {
Q_OBJECT
@ -48,15 +50,28 @@ public:
protected slots:
void stateChanged(LoggingPlugin::State state);
void setBeginAndEndTimes(quint32 startTimeStamp, quint32 endTimeStamp);
void setPlaybackPosition(quint32 positionTimeStamp);
void playButtonAction();
void pauseButtonAction();
void stopButtonAction();
void enableWidgets();
void disableWidgets();
void sliderMoved(int);
void sliderAction();
signals:
void pause();
void play();
void resumeReplay(quint32 positionTimeStamp);
void pauseReplay();
void pauseReplayAndResetPosition();
private:
Ui_Logging *m_logging;
LoggingPlugin *loggingPlugin;
ScopeGadgetFactory *scpPlugin;
QTimer sliderActionDelay;
void updatePositionLabel(quint32 positionTimeStamp);
};
#endif /* LoggingGADGETWIDGET_H_ */

View File

@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/logging">
<file>images/play.png</file>
<file>images/pause.png</file>
<file>images/stop.png</file>
</qresource>
</RCC>