Commit 72dd2b00 authored by Lorenz Meier's avatar Lorenz Meier

Merge pull request #638 from Susurrus/logging_compress

QGC data logging now records small MAVLink messages.
parents f27f6c7f 321f21f2
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QSettings> #include <QSettings>
#include <QDesktopServices> #include <QDesktopServices>
#include <QtEndian>
#include "MAVLinkProtocol.h" #include "MAVLinkProtocol.h"
#include "UASInterface.h" #include "UASInterface.h"
...@@ -358,19 +359,26 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) ...@@ -358,19 +359,26 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b)
// Log data // Log data
if (m_loggingEnabled && m_logfile) if (m_loggingEnabled && m_logfile)
{ {
uint8_t buf[MAVLINK_MAX_PACKET_LEN+sizeof(quint64)] = {0}; uint8_t buf[MAVLINK_MAX_PACKET_LEN+sizeof(quint64)];
quint64 time = QGC::groundTimeUsecs();
memcpy(buf, (void*)&time, sizeof(quint64)); // Write the uint64 time in microseconds in big endian format before the message.
// Write message to buffer // This timestamp is saved in UTC time. We are only saving in ms precision because
mavlink_msg_to_send_buffer(buf+sizeof(quint64), &message); // getting more than this isn't possible with Qt without a ton of extra code.
//we need to write the maximum package length for having a quint64 time = (quint64)QDateTime::currentMSecsSinceEpoch() * 1000;
//consistent file structure and beeing able to parse it again qToBigEndian(time, buf);
int len = MAVLINK_MAX_PACKET_LEN + sizeof(quint64);
// Then write the message to the buffer
int len = mavlink_msg_to_send_buffer(buf + sizeof(quint64), &message);
// Determine how many bytes were written by adding the timestamp size to the message size
len += sizeof(quint64);
// Now write this timestamp/message pair to the log.
QByteArray b((const char*)buf, len); QByteArray b((const char*)buf, len);
if(m_logfile->write(b) != len) if(m_logfile->write(b) != len)
{ {
// If there's an error logging data, raise an alert and stop logging.
emit protocolStatusMessage(tr("MAVLink Logging failed"), tr("Could not write to file %1, disabling logging.").arg(m_logfile->fileName())); emit protocolStatusMessage(tr("MAVLink Logging failed"), tr("Could not write to file %1, disabling logging.").arg(m_logfile->fileName()));
// Stop logging
enableLogging(false); enableLogging(false);
} }
} }
......
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QDesktopServices> #include <QDesktopServices>
#include <QtEndian>
#include "MainWindow.h" #include "MainWindow.h"
#include "SerialLink.h" #include "SerialLink.h"
...@@ -10,17 +11,15 @@ ...@@ -10,17 +11,15 @@
QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *parent) : QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *parent) :
QWidget(parent), QWidget(parent),
lineCounter(0), playbackStartTime(0),
totalLines(0), logStartTime(0),
startTime(0), logEndTime(0),
endTime(0),
currentStartTime(0),
accelerationFactor(1.0f), accelerationFactor(1.0f),
mavlink(mavlink), mavlink(mavlink),
logLink(NULL), logLink(NULL),
loopCounter(0), loopCounter(0),
mavlinkLogFormat(true), mavlinkLogFormat(true),
binaryBaudRate(57600), binaryBaudRate(defaultBinaryBaudRate),
isPlaying(false), isPlaying(false),
currPacketCount(0), currPacketCount(0),
lastLogDirectory(QDesktopServices::storageLocation(QDesktopServices::DesktopLocation)), lastLogDirectory(QDesktopServices::storageLocation(QDesktopServices::DesktopLocation)),
...@@ -44,7 +43,7 @@ QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *pare ...@@ -44,7 +43,7 @@ QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *pare
setAccelerationFactorInt(49); setAccelerationFactorInt(49);
ui->speedSlider->setValue(49); ui->speedSlider->setValue(49);
ui->positionSlider->setValue(ui->positionSlider->minimum()); updatePositionSliderUi(0.0);
ui->playButton->setEnabled(false); ui->playButton->setEnabled(false);
ui->speedSlider->setEnabled(false); ui->speedSlider->setEnabled(false);
...@@ -53,6 +52,9 @@ QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *pare ...@@ -53,6 +52,9 @@ QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *pare
ui->logFileNameLabel->setEnabled(false); ui->logFileNameLabel->setEnabled(false);
ui->logStatsLabel->setEnabled(false); ui->logStatsLabel->setEnabled(false);
// Monitor for when the end of the log file is reached. This is done using signals because the main work is in a timer.
connect(this, SIGNAL(logFileEndReached()), &loopTimer, SLOT(stop()));
loadSettings(); loadSettings();
} }
...@@ -90,14 +92,18 @@ void QGCMAVLinkLogPlayer::play() ...@@ -90,14 +92,18 @@ void QGCMAVLinkLogPlayer::play()
{ {
if (logFile.isOpen()) if (logFile.isOpen())
{ {
// Disable the log file selector button
ui->selectFileButton->setEnabled(false); ui->selectFileButton->setEnabled(false);
if (logLink)
// Make sure we aren't at the end of the file, if we are, reset to the beginning and play from there.
if (logFile.atEnd())
{ {
logLink->disconnect(); reset();
LinkManager::instance()->removeLink(logLink);
delete logLink;
} }
logLink = new MAVLinkSimulationLink("");
// Always correct the current start time such that the next message will play immediately at playback.
// We do this by subtracting the current file playback offset from now()
playbackStartTime = (quint64)QDateTime::currentMSecsSinceEpoch() - (logCurrentTime - logStartTime) / 1000;
// Start timer // Start timer
if (mavlinkLogFormat) if (mavlinkLogFormat)
...@@ -112,10 +118,10 @@ void QGCMAVLinkLogPlayer::play() ...@@ -112,10 +118,10 @@ void QGCMAVLinkLogPlayer::play()
// to guarantee the baud rate, then divide 1000 by the number of read // to guarantee the baud rate, then divide 1000 by the number of read
// operations to obtain the interval in milliseconds // operations to obtain the interval in milliseconds
int interval = 1000 / ((binaryBaudRate / 10) / len); int interval = 1000 / ((binaryBaudRate / 10) / len);
loopTimer.start(interval*accelerationFactor); loopTimer.start(interval / accelerationFactor);
} }
isPlaying = true; isPlaying = true;
ui->logStatsLabel->setText(tr("Started playing..")); ui->playButton->setChecked(true);
ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-pause.svg")); ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-pause.svg"));
} }
else else
...@@ -133,44 +139,97 @@ void QGCMAVLinkLogPlayer::play() ...@@ -133,44 +139,97 @@ void QGCMAVLinkLogPlayer::play()
void QGCMAVLinkLogPlayer::pause() void QGCMAVLinkLogPlayer::pause()
{ {
isPlaying = false;
loopTimer.stop(); loopTimer.stop();
isPlaying = false;
ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-start.svg")); ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-start.svg"));
ui->playButton->setChecked(false);
ui->selectFileButton->setEnabled(true); ui->selectFileButton->setEnabled(true);
if (logLink)
{
logLink->disconnect();
LinkManager::instance()->removeLink(logLink);
delete logLink;
logLink = NULL;
}
} }
bool QGCMAVLinkLogPlayer::reset(int packetIndex) void QGCMAVLinkLogPlayer::reset()
{
pause();
loopCounter = 0;
logFile.reset();
// Now update the position slider to its default location
updatePositionSliderUi(0.0);
// And since we haven't starting playback, clear the time of initial playback and the current timestamp.
playbackStartTime = 0;
logCurrentTime = logStartTime;
}
bool QGCMAVLinkLogPlayer::jumpToPlaybackLocation(float percentage)
{ {
// Reset only for valid values // Reset only for valid values
const unsigned int packetSize = timeLen + packetLen; if (percentage <= 100.0f && percentage >= 0.0f)
if (packetIndex >= 0 && packetIndex*packetSize <= logFile.size() - packetSize)
{ {
bool result = true; bool result = true;
pause(); if (mavlinkLogFormat)
loopCounter = 0; {
logFile.reset(); // But if we have a timestamped MAVLink log, then actually aim to hit that percentage in terms of
// time through the file.
qint64 newFilePos = (qint64)(percentage * (float)logFile.size());
// Now seek to the appropriate position, failing gracefully if we can't.
if (!logFile.seek(newFilePos))
{
// Fallback: Start from scratch
logFile.reset();
ui->logStatsLabel->setText(tr("Changing packet index failed, back to start."));
result = false;
}
// But we do align to the next MAVLink message for consistency.
mavlink_message_t dummy;
logCurrentTime = findNextMavlinkMessage(&dummy);
if (!logFile.seek(packetIndex*packetSize)) // Now calculate the current file location based on time.
float newRelativeTime = (float)(logCurrentTime - logStartTime);
// Calculate the effective baud rate of the file in bytes/s.
float logDuration = (logEndTime - logStartTime);
float baudRate = logFile.size() / logDuration / 1e6;
// And the desired time is:
float desiredTime = percentage * logDuration;
// And now jump the necessary number of bytes in the proper direction
qint64 offset = (newRelativeTime - desiredTime) * baudRate;
logFile.seek(logFile.pos() + offset);
// And scan until we reach the start of a MAVLink message. We make sure to record this timestamp for
// smooth jumping around the file.
logCurrentTime = findNextMavlinkMessage(&dummy);
// Now update the UI with our actual final position.
newRelativeTime = (float)(logCurrentTime - logStartTime);
percentage = newRelativeTime / logDuration;
updatePositionSliderUi(percentage);
}
else
{ {
// Fallback: Start from scratch // If we're working with a non-timestamped file, we just jump to that percentage of the file,
logFile.reset(); // align to the next MAVLink message and roll with it. No reason to do anything more complicated.
ui->logStatsLabel->setText(tr("Changing packet index failed, back to start.")); qint64 newFilePos = (qint64)(percentage * (float)logFile.size());
result = false;
// Now seek to the appropriate position, failing gracefully if we can't.
if (!logFile.seek(newFilePos))
{
// Fallback: Start from scratch
logFile.reset();
ui->logStatsLabel->setText(tr("Changing packet index failed, back to start."));
result = false;
}
// But we do align to the next MAVLink message for consistency.
mavlink_message_t dummy;
findNextMavlinkMessage(&dummy);
} }
ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-start.svg")); // Now update the UI. This is necessary because stop() is called when loading a new logfile
ui->positionSlider->blockSignals(true);
int sliderVal = (packetIndex / (double)(logFile.size()/packetSize)) * (ui->positionSlider->maximum() - ui->positionSlider->minimum());
ui->positionSlider->setValue(sliderVal);
ui->positionSlider->blockSignals(false);
startTime = 0;
return result; return result;
} }
else else
...@@ -179,6 +238,26 @@ bool QGCMAVLinkLogPlayer::reset(int packetIndex) ...@@ -179,6 +238,26 @@ bool QGCMAVLinkLogPlayer::reset(int packetIndex)
} }
} }
void QGCMAVLinkLogPlayer::updatePositionSliderUi(float percent)
{
ui->positionSlider->blockSignals(true);
int sliderVal = ui->positionSlider->minimum() + (int)(percent * (float)(ui->positionSlider->maximum() - ui->positionSlider->minimum()));
ui->positionSlider->setValue(sliderVal);
// Calculate the runtime in hours:minutes:seconds
// WARNING: Order matters in this computation
quint32 seconds = percent * (logEndTime - logStartTime) / 1e6;
quint32 minutes = seconds / 60;
quint32 hours = minutes / 60;
seconds -= 60*minutes;
minutes -= 60*hours;
// And show the user the details we found about this file.
QString timeLabel = tr("%1h:%2m:%3s").arg(hours, 2).arg(minutes, 2).arg(seconds, 2);
ui->positionSlider->setToolTip(timeLabel);
ui->positionSlider->blockSignals(false);
}
void QGCMAVLinkLogPlayer::loadSettings() void QGCMAVLinkLogPlayer::loadSettings()
{ {
QSettings settings; QSettings settings;
...@@ -203,6 +282,7 @@ void QGCMAVLinkLogPlayer::storeSettings() ...@@ -203,6 +282,7 @@ void QGCMAVLinkLogPlayer::storeSettings()
*/ */
bool QGCMAVLinkLogPlayer::selectLogFile() bool QGCMAVLinkLogPlayer::selectLogFile()
{ {
// Prompt the user for a new file using the last directory they searched.
return selectLogFile(lastLogDirectory); return selectLogFile(lastLogDirectory);
} }
...@@ -253,11 +333,9 @@ void QGCMAVLinkLogPlayer::setAccelerationFactorInt(int factor) ...@@ -253,11 +333,9 @@ void QGCMAVLinkLogPlayer::setAccelerationFactorInt(int factor)
// operations to obtain the interval in milliseconds // operations to obtain the interval in milliseconds
int interval = 1000 / ((binaryBaudRate / 10) / len); int interval = 1000 / ((binaryBaudRate / 10) / len);
loopTimer.stop(); loopTimer.stop();
loopTimer.start(interval/accelerationFactor); loopTimer.start(interval / accelerationFactor);
} }
//qDebug() << "FACTOR:" << accelerationFactor;
ui->speedLabel->setText(tr("Speed: %1X").arg(accelerationFactor, 5, 'f', 2, '0')); ui->speedLabel->setText(tr("Speed: %1X").arg(accelerationFactor, 5, 'f', 2, '0'));
} }
...@@ -271,21 +349,24 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file) ...@@ -271,21 +349,24 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file)
ui->logFileNameLabel->setEnabled(true); ui->logFileNameLabel->setEnabled(true);
ui->logStatsLabel->setEnabled(true); ui->logStatsLabel->setEnabled(true);
// Check if logging is still enabled // Disable logging while replaying a log file.
if (mavlink->loggingEnabled()) if (mavlink->loggingEnabled())
{ {
mavlink->enableLogging(false); mavlink->enableLogging(false);
MainWindow::instance()->showInfoMessage(tr("MAVLink Logging Stopped during Replay"), tr("MAVLink logging has been stopped during the log replay. To re-enable logging, use the link properties in the communication menu.")); MainWindow::instance()->showInfoMessage(tr("MAVLink Logging Stopped during Replay"), tr("MAVLink logging has been stopped during the log replay. To re-enable logging, use the link properties in the communication menu."));
} }
// Ensure that the playback process is stopped // Make sure to stop the logging process and reset everything.
reset();
// And that the old file is closed nicely.
if (logFile.isOpen()) if (logFile.isOpen())
{ {
pause();
logFile.close(); logFile.close();
} }
logFile.setFileName(file);
// Now load the new file.
logFile.setFileName(file);
if (!logFile.open(QFile::ReadOnly)) if (!logFile.open(QFile::ReadOnly))
{ {
MainWindow::instance()->showCriticalMessage(tr("The selected logfile is unreadable"), tr("Please make sure that the file %1 is readable or select a different file").arg(file)); MainWindow::instance()->showCriticalMessage(tr("The selected logfile is unreadable"), tr("Please make sure that the file %1 is readable or select a different file").arg(file));
...@@ -296,47 +377,81 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file) ...@@ -296,47 +377,81 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file)
{ {
QFileInfo logFileInfo(file); QFileInfo logFileInfo(file);
logFile.reset(); logFile.reset();
startTime = 0; ui->logFileNameLabel->setText(tr("Logfile: %1").arg(logFileInfo.fileName()));
ui->logFileNameLabel->setText(tr("%1").arg(logFileInfo.baseName()));
// If there's an existing MAVLinkSimulationLink() being used for an old file,
// we replace it.
if (logLink)
{
logLink->disconnect();
LinkManager::instance()->removeLink(logLink);
delete logLink;
}
logLink = new MAVLinkSimulationLink("");
// Select if binary or MAVLink log format is used // Select if binary or MAVLink log format is used
mavlinkLogFormat = file.endsWith(".mavlink"); mavlinkLogFormat = file.endsWith(".mavlink");
if (mavlinkLogFormat) if (mavlinkLogFormat)
{ {
// Get the time interval from the logfile // Get the first timestamp from the logfile
// This should be a big-endian uint64.
QByteArray timestamp = logFile.read(timeLen); QByteArray timestamp = logFile.read(timeLen);
quint64 starttime = parseTimestamp(timestamp);
// Now find the last timestamp by scanning for the last MAVLink packet and
// find the timestamp before it. To do this we start searchin a little before
// the end of the file, specifically the maximum MAVLink packet size + the
// timestamp size. This guarantees that we will hit a MAVLink packet before
// the end of the file. Unfortunately, it basically guarantees that we will
// hit more than one. This is why we have to search for a bit.
qint64 fileLoc = logFile.size() - MAVLINK_MAX_PACKET_LEN - timeLen;
logFile.seek(fileLoc);
quint64 endtime = starttime; // Set a sane default for the endtime
mavlink_message_t msg;
quint64 newTimestamp;
while ((newTimestamp = findNextMavlinkMessage(&msg)) > endtime) {
endtime = newTimestamp;
}
if (endtime == starttime) {
MainWindow::instance()->showCriticalMessage(tr("The selected logfile cannot be processed"), tr("No valid timestamps were found at the end of the logfile.").arg(file));
logFile.setFileName("");
ui->logFileNameLabel->setText(tr("No logfile selected"));
return false;
}
// First timestamp // Remember the start and end time so we can move around this logfile with the slider.
quint64 starttime = *((quint64*)(timestamp.constData())); logEndTime = endtime;
logStartTime = starttime;
logCurrentTime = logStartTime;
// Last timestamp // Reset our log file so when we go to read it for the first time, we start at the beginning.
logFile.seek(logFile.size()-packetLen-timeLen);
QByteArray timestamp2 = logFile.read(timeLen);
quint64 endtime = *((quint64*)(timestamp2.constData()));
// Reset everything
logFile.reset(); logFile.reset();
qDebug() << "Starttime:" << starttime << "End:" << endtime; // Calculate the runtime in hours:minutes:seconds
// WARNING: Order matters in this computation // WARNING: Order matters in this computation
int seconds = (endtime - starttime)/1000000; quint32 seconds = (endtime - starttime)/1000000;
int minutes = seconds / 60; quint32 minutes = seconds / 60;
int hours = minutes / 60; quint32 hours = minutes / 60;
seconds -= 60*minutes; seconds -= 60*minutes;
minutes -= 60*hours; minutes -= 60*hours;
// And show the user the details we found about this file.
QString timelabel = tr("%1h:%2m:%3s").arg(hours, 2).arg(minutes, 2).arg(seconds, 2); QString timelabel = tr("%1h:%2m:%3s").arg(hours, 2).arg(minutes, 2).arg(seconds, 2);
currPacketCount = logFileInfo.size()/(MAVLINK_MAX_PACKET_LEN+sizeof(quint64)); currPacketCount = logFileInfo.size()/(32 + MAVLINK_NUM_NON_PAYLOAD_BYTES + sizeof(quint64)); // Count packets by assuming an average payload size of 32 bytes
ui->logStatsLabel->setText(tr("%2 MB, %3 packets, %4").arg(logFileInfo.size()/1000000.0f, 0, 'f', 2).arg(currPacketCount).arg(timelabel)); ui->logStatsLabel->setText(tr("%2 MB, ~%3 packets, %4").arg(logFileInfo.size()/1000000.0f, 0, 'f', 2).arg(currPacketCount).arg(timelabel));
} }
else else
{ {
// Load in binary mode // Load in binary mode. In this mode, files should be have a filename postfix
// of the baud rate they were recorded at, like `test_run_115200.bin`. Then on
// playback, the datarate is equal to set to this value.
// Set baud rate if any present // Set baud rate if any present. Otherwise we default to 57600.
QStringList parts = logFileInfo.baseName().split("_"); QStringList parts = logFileInfo.baseName().split("_");
binaryBaudRate = defaultBinaryBaudRate;
if (parts.count() > 1) if (parts.count() > 1)
{ {
bool ok; bool ok;
...@@ -359,9 +474,6 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file) ...@@ -359,9 +474,6 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file)
ui->logStatsLabel->setText(tr("%2 MB, %4 at %5 KB/s").arg(logFileInfo.size()/1000000.0f, 0, 'f', 2).arg(timelabel).arg(binaryBaudRate/10.0f/1024.0f, 0, 'f', 2)); ui->logStatsLabel->setText(tr("%2 MB, %4 at %5 KB/s").arg(logFileInfo.size()/1000000.0f, 0, 'f', 2).arg(timelabel).arg(binaryBaudRate/10.0f/1024.0f, 0, 'f', 2));
} }
// Reset current state
reset(0);
// Check if a serial link is connected // Check if a serial link is connected
bool linkWarning = false; bool linkWarning = false;
...@@ -382,24 +494,66 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file) ...@@ -382,24 +494,66 @@ bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file)
} }
} }
quint64 QGCMAVLinkLogPlayer::parseTimestamp(const QByteArray &data)
{
// Retrieve the timestamp from the ByteArray assuming a proper BigEndian quint64 timestamp in microseconds.
quint64 timestamp = qFromBigEndian(*((quint64*)(data.constData())));
// And get the current time in microseconds
quint64 currentTimestamp = ((quint64)QDateTime::currentMSecsSinceEpoch()) * 1000;
// Now if the parsed timestamp is in the future, it must be an old file where the timestamp was stored as
// little endian, so switch it.
if (timestamp > currentTimestamp) {
timestamp = qbswap(timestamp);
}
return timestamp;
}
/** /**
* Jumps to the current percentage of the position slider * Jumps to the current percentage of the position slider. When this is called, the LogPlayer should already
* have been paused, so it just jumps to the proper location in the file and resumes playing.
*/ */
void QGCMAVLinkLogPlayer::jumpToSliderVal(int slidervalue) void QGCMAVLinkLogPlayer::jumpToSliderVal(int slidervalue)
{ {
loopTimer.stop(); // Determine what percentage through the file we should be (time or packet number depending).
// Set the logfile to the correct percentage and float newLocation = slidervalue / (float)(ui->positionSlider->maximum() - ui->positionSlider->minimum());
// align to the timestamp values
int packetCount = logFile.size() / (packetLen + timeLen); // And clamp our calculated values to the valid range of [0,100]
int packetIndex = (packetCount - 1) * (slidervalue / (double)(ui->positionSlider->maximum() - ui->positionSlider->minimum())); if (newLocation > 100.0f)
{
newLocation = 100.0f;
}
if (newLocation < 0.0f)
{
newLocation = 0.0f;
}
// Do only accept valid jumps // Do only valid jumps
if (reset(packetIndex)) if (jumpToPlaybackLocation(newLocation))
{ {
if (mavlinkLogFormat) if (mavlinkLogFormat)
{ {
ui->logStatsLabel->setText(tr("Jumped to packet %1").arg(packetIndex)); // Grab the total seconds of this file (1e6 is due to microsecond -> second conversion)
int seconds = newLocation * (logEndTime - logStartTime) / 1e6;
int minutes = seconds / 60;
int hours = minutes / 60;
seconds -= 60*minutes;
minutes -= 60*hours;
ui->logStatsLabel->setText(tr("Jumped to time %1h:%2m:%3s").arg(hours, 2).arg(minutes, 2).arg(seconds, 2));
}
else
{
ui->logStatsLabel->setText(tr("Jumped to %1").arg(newLocation));
} }
play();
}
else
{
reset();
} }
} }
...@@ -413,103 +567,50 @@ void QGCMAVLinkLogPlayer::jumpToSliderVal(int slidervalue) ...@@ -413,103 +567,50 @@ void QGCMAVLinkLogPlayer::jumpToSliderVal(int slidervalue)
*/ */
void QGCMAVLinkLogPlayer::logLoop() void QGCMAVLinkLogPlayer::logLoop()
{ {
// If we have a file with timestamps, try and pace this out following the time differences
// between the timestamps and the current playback speed.
if (mavlinkLogFormat) if (mavlinkLogFormat)
{ {
bool ok; // Now parse MAVLink messages, grabbing their timestamps as we go. We stop once we
// have at least 3ms until the next one.
int nextExecutionTime = 0;
mavlink_message_t msg;
while (nextExecutionTime < 3) {
// First check initialization // Now we're sitting at the start of a MAVLink message, so read it all into a byte array for feeding to our parser.
if (startTime == 0) QByteArray message = logFile.read(msg.len + MAVLINK_NUM_NON_PAYLOAD_BYTES);
{
QByteArray startBytes = logFile.read(timeLen);
// Check if the correct number of bytes could be read // Emit this message to our MAVLink parser.
if (startBytes.length() != timeLen) emit bytesReady(logLink, message);
{
ui->logStatsLabel->setText(tr("Error reading first %1 bytes").arg(timeLen));
MainWindow::instance()->showCriticalMessage(tr("Failed loading MAVLink Logfile"), tr("Error reading first %1 bytes from logfile. Got %2 instead of %1 bytes. Is the logfile readable?").arg(timeLen).arg(startBytes.length()));
reset();
return;
}
// Convert data to timestamp // If we've reached the end of the of the file, make sure we handle that well
startTime = *((quint64*)(startBytes.constData())); if (logFile.atEnd())
currentStartTime = QGC::groundTimeUsecs();
ok = true;
//qDebug() << "START TIME: " << startTime;
// Check if these bytes could be correctly decoded
// TODO
if (!ok)
{ {
ui->logStatsLabel->setText(tr("Error decoding first timestamp, aborting.")); // For some reason calling pause() here doesn't work, so we update the UI manually here.
MainWindow::instance()->showCriticalMessage(tr("Failed loading MAVLink Logfile"), tr("Could not load initial timestamp from file %1. Is the file corrupted?").arg(logFile.fileName())); isPlaying = false;
reset(); ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-start.svg"));
ui->playButton->setChecked(false);
ui->selectFileButton->setEnabled(true);
// Note that we explicitly set the slider to 100%, as it may not hit that by itself depending on log file size.
updatePositionSliderUi(100.0f);
emit logFileEndReached();
return; return;
} }
}
// Initialization seems fine, load next chunk // Run our parser to find the next timestamp and leave us at the start of the next MAVLink message.
//this is ok because before we already read the timestamp of this paket before logCurrentTime = findNextMavlinkMessage(&msg);
QByteArray chunk = logFile.read(timeLen+packetLen);
QByteArray packet = chunk.left(packetLen);
// Emit this packet // Calculate how long we should wait in real time until parsing this message.
emit bytesReady(logLink, packet); // We pace ourselves relative to the start time of playback to fix any drift (initially set in play())
qint64 timediff = (logCurrentTime - logStartTime) / accelerationFactor;
// Check if reached end of file before reading next timestamp quint64 desiredPacedTime = playbackStartTime + ((quint64)timediff) / 1000;
if (chunk.length() < (timeLen + packetLen) || logFile.atEnd()) quint64 currentTime = (quint64)QDateTime::currentMSecsSinceEpoch();
{ nextExecutionTime = desiredPacedTime - currentTime;
// Reached end of file
reset();
QString status = tr("Reached end of MAVLink log file.");
ui->logStatsLabel->setText(status);
MainWindow::instance()->showStatusMessage(status);
return;
} }
// End of file not reached, read next timestamp // And schedule the next execution of this function.
// which is located after current packet loopTimer.start(nextExecutionTime);
QByteArray rawTime = chunk.mid(packetLen);
// This is the timestamp of the next packet
quint64 time = *((quint64*)(rawTime.constData()));
ok = true;
if (!ok)
{
// Convert it to 64bit number
QString status = tr("Time conversion error during log replay. Continuing..");
ui->logStatsLabel->setText(status);
MainWindow::instance()->showStatusMessage(status);
}
else
{
// Normal processing, passed all checks
// start timer to match time offset between
// this and next packet
// Offset in ms
qint64 timediff = (time - startTime)/accelerationFactor;
// Immediately load any data within
// a 3 ms interval
int nextExecutionTime = (((qint64)currentStartTime + (qint64)timediff) - (qint64)QGC::groundTimeUsecs())/1000;
//qDebug() << "nextExecutionTime:" << nextExecutionTime << "QGC START TIME:" << currentStartTime << "LOG START TIME:" << startTime;
if (nextExecutionTime < 2)
{
logLoop();
}
else
{
loopTimer.start(nextExecutionTime);
}
}
} }
else else
{ {
...@@ -532,23 +633,48 @@ void QGCMAVLinkLogPlayer::logLoop() ...@@ -532,23 +633,48 @@ void QGCMAVLinkLogPlayer::logLoop()
return; return;
} }
} }
// Ui update: Only every 20 messages
// to prevent flickering and high CPU load
// Update status label // Update the UI every 2^5=32 times, or when there isn't much data to be played back.
// Update progress bar // Reduces flickering and minimizes CPU load.
if (loopCounter % 40 == 0 || currPacketCount < 500) if ((loopCounter & 0x1F) == 0 || currPacketCount < 2000)
{ {
QFileInfo logFileInfo(logFile); QFileInfo logFileInfo(logFile);
int progress = (ui->positionSlider->maximum()-ui->positionSlider->minimum())*(logFile.pos()/static_cast<float>(logFileInfo.size())); updatePositionSliderUi(logFile.pos() / static_cast<float>(logFileInfo.size()));
//qDebug() << "Progress:" << progress;
ui->positionSlider->blockSignals(true);
ui->positionSlider->setValue(progress);
ui->positionSlider->blockSignals(false);
} }
loopCounter++; loopCounter++;
} }
/**
* This function parses out the next MAVLink message and its corresponding timestamp.
*
* It makes no assumptions about where in the file we currently are. It leaves the file right
* at the beginning of the successfully parsed message. Note that this function will not attempt to
* correct for any MAVLink parsing failures, so it always returns the next successfully-parsed
* message.
*
* @param msg[output] Where the final parsed message output will go.
* @return A Unix timestamp in microseconds UTC or 0 if parsing failed
*/
quint64 QGCMAVLinkLogPlayer::findNextMavlinkMessage(mavlink_message_t *msg)
{
char nextByte;
mavlink_status_t comm;
while (logFile.getChar(&nextByte)) { // Loop over every byte
bool messageFound = mavlink_parse_char(logLink->getId(), nextByte, msg, &comm);
// If we've found a message, jump back to the start of the message, grab the timestamp,
// and go back to the end of this file.
if (messageFound) {
logFile.seek(logFile.pos() - (msg->len + MAVLINK_NUM_NON_PAYLOAD_BYTES + timeLen));
QByteArray rawTime = logFile.read(timeLen);
return parseTimestamp(rawTime);
}
}
// Otherwise, if we never find a message, return a failure code of 0.
return 0;
}
void QGCMAVLinkLogPlayer::changeEvent(QEvent *e) void QGCMAVLinkLogPlayer::changeEvent(QEvent *e)
{ {
QWidget::changeEvent(e); QWidget::changeEvent(e);
...@@ -571,4 +697,4 @@ void QGCMAVLinkLogPlayer::paintEvent(QPaintEvent *) ...@@ -571,4 +697,4 @@ void QGCMAVLinkLogPlayer::paintEvent(QPaintEvent *)
opt.init(this); opt.init(this);
QPainter p(this); QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
} }
\ No newline at end of file
...@@ -52,10 +52,10 @@ public slots: ...@@ -52,10 +52,10 @@ public slots:
void playPause(bool play); void playPause(bool play);
/** @brief Replay the logfile */ /** @brief Replay the logfile */
void play(); void play();
/** @brief Pause the logfile */ /** @brief Pause the log player. */
void pause(); void pause();
/** @brief Reset the logfile */ /** @brief Reset the internal log player state, including the UI */
bool reset(int packetIndex=0); void reset();
/** @brief Select logfile */ /** @brief Select logfile */
bool selectLogFile(const QString startDirectory); bool selectLogFile(const QString startDirectory);
/** @brief Select logfile */ /** @brief Select logfile */
...@@ -72,21 +72,22 @@ public slots: ...@@ -72,21 +72,22 @@ public slots:
signals: signals:
/** @brief Send ready bytes */ /** @brief Send ready bytes */
void bytesReady(LinkInterface* link, const QByteArray& bytes); void bytesReady(LinkInterface* link, const QByteArray& bytes);
void logFileEndReached();
protected: protected:
int lineCounter; quint64 playbackStartTime; ///< The time when the logfile was first played back. This is used to pace out replaying the messages to fix long-term drift/skew. 0 indicates that the player hasn't initiated playback of this log file. In units of milliseconds since epoch UTC.
int totalLines; quint64 logCurrentTime; ///< The timestamp of the next message in the log file. In units of microseconds since epoch UTC.
quint64 startTime; quint64 logStartTime; ///< The first timestamp in the current log file. In units of microseconds since epoch UTC.
quint64 endTime; quint64 logEndTime; ///< The last timestamp in the current log file. In units of microseconds since epoch UTC.
quint64 currentStartTime;
float accelerationFactor; float accelerationFactor;
MAVLinkProtocol* mavlink; MAVLinkProtocol* mavlink;
MAVLinkSimulationLink* logLink; MAVLinkSimulationLink* logLink;
QFile logFile; QFile logFile;
QTimer loopTimer; QTimer loopTimer;
int loopCounter; int loopCounter;
bool mavlinkLogFormat; bool mavlinkLogFormat; ///< If the logfile is stored in the timestamped MAVLink log format
int binaryBaudRate; int binaryBaudRate;
static const int defaultBinaryBaudRate = 57600;
bool isPlaying; bool isPlaying;
unsigned int currPacketCount; unsigned int currPacketCount;
static const int packetLen = MAVLINK_MAX_PACKET_LEN; static const int packetLen = MAVLINK_MAX_PACKET_LEN;
...@@ -100,6 +101,35 @@ protected: ...@@ -100,6 +101,35 @@ protected:
private: private:
Ui::QGCMAVLinkLogPlayer *ui; Ui::QGCMAVLinkLogPlayer *ui;
virtual void paintEvent(QPaintEvent *); virtual void paintEvent(QPaintEvent *);
/** @brief Parse out a quint64 timestamp in microseconds in the proper endianness. */
quint64 parseTimestamp(const QByteArray &data);
/**
* This function parses out the next MAVLink message and its corresponding timestamp.
*
* It makes no assumptions about where in the file we currently are. It leaves the file right
* at the beginning of the successfully parsed message. Note that this function will not attempt to
* correct for any MAVLink parsing failures, so it always returns the next successfully-parsed
* message.
*
* @param msg[output] Where the final parsed message output will go.
* @return A Unix timestamp in microseconds UTC or 0 if parsing failed
*/
quint64 findNextMavlinkMessage(mavlink_message_t *msg);
/**
* Updates the QSlider UI to be at the given percentage.
* @param percent A percentage value between 0.0% and 100.0%.
*/
void updatePositionSliderUi(float percent);
/**
* Jumps to a new position in the current playback file as a percentage.
* @param percentage The position of the file to jump to as a percentage.
* @return True if the new file position was successfully jumped to, false otherwise
*/
bool jumpToPlaybackLocation(float percentage);
}; };
#endif // QGCMAVLINKLOGPLAYER_H #endif // QGCMAVLINKLOGPLAYER_H
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
<item> <item>
<widget class="QLabel" name="logStatsLabel"> <widget class="QLabel" name="logStatsLabel">
<property name="text"> <property name="text">
<string>No logfile selected..</string> <string/>
</property> </property>
</widget> </widget>
</item> </item>
...@@ -54,6 +54,16 @@ ...@@ -54,6 +54,16 @@
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="timeLabel">
<property name="text">
<string>Time</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>
...@@ -119,7 +129,7 @@ ...@@ -119,7 +129,7 @@
<item> <item>
<widget class="QLabel" name="logFileNameLabel"> <widget class="QLabel" name="logFileNameLabel">
<property name="text"> <property name="text">
<string>-</string> <string>No logfile selected..</string>
</property> </property>
</widget> </widget>
</item> </item>
......
...@@ -37,7 +37,7 @@ QGCStatusBar::QGCStatusBar(QWidget *parent) : ...@@ -37,7 +37,7 @@ QGCStatusBar::QGCStatusBar(QWidget *parent) :
{ {
setObjectName("QGC_STATUSBAR"); setObjectName("QGC_STATUSBAR");
toggleLoggingButton = new QPushButton("Logging", this); toggleLoggingButton = new QPushButton(tr("Log to file"), this);
toggleLoggingButton->setCheckable(true); toggleLoggingButton->setCheckable(true);
addPermanentWidget(toggleLoggingButton); addPermanentWidget(toggleLoggingButton);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment