You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
475 lines
15 KiB
475 lines
15 KiB
#include <QFileDialog> |
|
#include <QMessageBox> |
|
#include <QDesktopServices> |
|
|
|
#include "MainWindow.h" |
|
#include "QGCMAVLinkLogPlayer.h" |
|
#include "QGC.h" |
|
#include "ui_QGCMAVLinkLogPlayer.h" |
|
|
|
QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *parent) : |
|
QWidget(parent), |
|
lineCounter(0), |
|
totalLines(0), |
|
startTime(0), |
|
endTime(0), |
|
currentStartTime(0), |
|
accelerationFactor(1.0f), |
|
mavlink(mavlink), |
|
logLink(NULL), |
|
loopCounter(0), |
|
mavlinkLogFormat(true), |
|
binaryBaudRate(57600), |
|
isPlaying(false), |
|
ui(new Ui::QGCMAVLinkLogPlayer) |
|
{ |
|
ui->setupUi(this); |
|
ui->gridLayout->setAlignment(Qt::AlignTop); |
|
|
|
// Connect protocol |
|
connect(this, SIGNAL(bytesReady(LinkInterface*,QByteArray)), mavlink, SLOT(receiveBytes(LinkInterface*,QByteArray))); |
|
|
|
// Setup timer |
|
connect(&loopTimer, SIGNAL(timeout()), this, SLOT(logLoop())); |
|
|
|
// Setup buttons |
|
connect(ui->selectFileButton, SIGNAL(clicked()), this, SLOT(selectLogFile())); |
|
connect(ui->playButton, SIGNAL(clicked()), this, SLOT(playPauseToggle())); |
|
connect(ui->speedSlider, SIGNAL(valueChanged(int)), this, SLOT(setAccelerationFactorInt(int))); |
|
connect(ui->positionSlider, SIGNAL(valueChanged(int)), this, SLOT(jumpToSliderVal(int))); |
|
connect(ui->positionSlider, SIGNAL(sliderPressed()), this, SLOT(pause())); |
|
|
|
setAccelerationFactorInt(49); |
|
ui->speedSlider->setValue(49); |
|
ui->positionSlider->setValue(ui->positionSlider->minimum()); |
|
} |
|
|
|
QGCMAVLinkLogPlayer::~QGCMAVLinkLogPlayer() |
|
{ |
|
delete ui; |
|
} |
|
|
|
void QGCMAVLinkLogPlayer::playPause(bool enabled) |
|
{ |
|
if (enabled) |
|
{ |
|
play(); |
|
} |
|
else |
|
{ |
|
pause(); |
|
} |
|
} |
|
|
|
void QGCMAVLinkLogPlayer::playPauseToggle() |
|
{ |
|
if (isPlaying) |
|
{ |
|
pause(); |
|
} |
|
else |
|
{ |
|
play(); |
|
} |
|
} |
|
|
|
void QGCMAVLinkLogPlayer::play() |
|
{ |
|
if (logFile.isOpen()) |
|
{ |
|
ui->selectFileButton->setEnabled(false); |
|
if (logLink) |
|
{ |
|
logLink->disconnect(); |
|
LinkManager::instance()->removeLink(logLink); |
|
delete logLink; |
|
} |
|
logLink = new MAVLinkSimulationLink(""); |
|
|
|
// Start timer |
|
if (mavlinkLogFormat) |
|
{ |
|
loopTimer.start(1); |
|
} |
|
else |
|
{ |
|
// Read len bytes at a time |
|
int len = 100; |
|
// Calculate the number of times to read 100 bytes per second |
|
// to guarantee the baud rate, then divide 1000 by the number of read |
|
// operations to obtain the interval in milliseconds |
|
int interval = 1000 / ((binaryBaudRate / 10) / len); |
|
loopTimer.start(interval*accelerationFactor); |
|
} |
|
isPlaying = true; |
|
ui->playButton->setIcon(QIcon(":images/actions/media-playback-pause.svg")); |
|
} |
|
else |
|
{ |
|
ui->playButton->setChecked(false); |
|
QMessageBox msgBox; |
|
msgBox.setIcon(QMessageBox::Information); |
|
msgBox.setText(tr("No logfile selected")); |
|
msgBox.setInformativeText(tr("Please select first a MAVLink log file before playing it.")); |
|
msgBox.setStandardButtons(QMessageBox::Ok); |
|
msgBox.setDefaultButton(QMessageBox::Ok); |
|
msgBox.exec(); |
|
} |
|
} |
|
|
|
void QGCMAVLinkLogPlayer::pause() |
|
{ |
|
isPlaying = false; |
|
loopTimer.stop(); |
|
ui->playButton->setIcon(QIcon(":images/actions/media-playback-start.svg")); |
|
ui->selectFileButton->setEnabled(true); |
|
if (logLink) |
|
{ |
|
logLink->disconnect(); |
|
LinkManager::instance()->removeLink(logLink); |
|
delete logLink; |
|
logLink = NULL; |
|
} |
|
} |
|
|
|
bool QGCMAVLinkLogPlayer::reset(int packetIndex) |
|
{ |
|
// Reset only for valid values |
|
const unsigned int packetSize = timeLen + packetLen; |
|
if (packetIndex >= 0 && packetIndex*packetSize <= logFile.size() - packetSize) |
|
{ |
|
bool result = true; |
|
pause(); |
|
loopCounter = 0; |
|
logFile.reset(); |
|
|
|
if (!logFile.seek(packetIndex*packetSize)) |
|
{ |
|
// Fallback: Start from scratch |
|
logFile.reset(); |
|
ui->logStatsLabel->setText(tr("Changing packet index failed, back to start.")); |
|
result = false; |
|
} |
|
|
|
ui->playButton->setIcon(QIcon(":images/actions/media-playback-start.svg")); |
|
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; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
void QGCMAVLinkLogPlayer::selectLogFile() |
|
{ |
|
QString fileName = QFileDialog::getOpenFileName(this, tr("Specify MAVLink log file name"), QDesktopServices::storageLocation(QDesktopServices::DesktopLocation), tr("MAVLink or Binary Logfile (*.mavlink *.bin *.log)")); |
|
|
|
loadLogFile(fileName); |
|
} |
|
|
|
/** |
|
* @param factor 0: 0.01X, 50: 1.0X, 100: 100.0X |
|
*/ |
|
void QGCMAVLinkLogPlayer::setAccelerationFactorInt(int factor) |
|
{ |
|
float f = factor+1.0f; |
|
f -= 50.0f; |
|
|
|
if (f < 0.0f) |
|
{ |
|
accelerationFactor = 1.0f / (-f/2.0f); |
|
} |
|
else |
|
{ |
|
accelerationFactor = 1+(f/2.0f); |
|
} |
|
|
|
// Update timer interval |
|
if (!mavlinkLogFormat) |
|
{ |
|
// Read len bytes at a time |
|
int len = 100; |
|
// Calculate the number of times to read 100 bytes per second |
|
// to guarantee the baud rate, then divide 1000 by the number of read |
|
// operations to obtain the interval in milliseconds |
|
int interval = 1000 / ((binaryBaudRate / 10) / len); |
|
loopTimer.stop(); |
|
loopTimer.start(interval/accelerationFactor); |
|
} |
|
|
|
//qDebug() << "FACTOR:" << accelerationFactor; |
|
|
|
ui->speedLabel->setText(tr("Speed: %1X").arg(accelerationFactor, 5, 'f', 2, '0')); |
|
} |
|
|
|
void QGCMAVLinkLogPlayer::loadLogFile(const QString& file) |
|
{ |
|
// Check if logging is still enabled |
|
if (mavlink->loggingEnabled()) |
|
{ |
|
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.")); |
|
} |
|
|
|
// Ensure that the playback process is stopped |
|
if (logFile.isOpen()) |
|
{ |
|
pause(); |
|
logFile.close(); |
|
} |
|
logFile.setFileName(file); |
|
|
|
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)); |
|
logFile.setFileName(""); |
|
} |
|
else |
|
{ |
|
QFileInfo logFileInfo(file); |
|
logFile.reset(); |
|
startTime = 0; |
|
ui->logFileNameLabel->setText(tr("%1").arg(logFileInfo.baseName())); |
|
|
|
// Select if binary or MAVLink log format is used |
|
mavlinkLogFormat = file.endsWith(".mavlink"); |
|
|
|
if (mavlinkLogFormat) |
|
{ |
|
// Get the time interval from the logfile |
|
QByteArray timestamp = logFile.read(timeLen); |
|
|
|
// First timestamp |
|
quint64 starttime = *((quint64*)(timestamp.constData())); |
|
|
|
// Last timestamp |
|
logFile.seek(logFile.size()-packetLen-timeLen); |
|
QByteArray timestamp2 = logFile.read(timeLen); |
|
quint64 endtime = *((quint64*)(timestamp2.constData())); |
|
// Reset everything |
|
logFile.reset(); |
|
|
|
qDebug() << "Starttime:" << starttime << "End:" << endtime; |
|
|
|
// WARNING: Order matters in this computation |
|
int seconds = (endtime - starttime)/1000000; |
|
int minutes = seconds / 60; |
|
int hours = minutes / 60; |
|
seconds -= 60*minutes; |
|
minutes -= 60*hours; |
|
|
|
QString timelabel = tr("%1h:%2m:%3s").arg(hours, 2).arg(minutes, 2).arg(seconds, 2); |
|
ui->logStatsLabel->setText(tr("%2 MB, %3 packets, %4").arg(logFileInfo.size()/1000000.0f, 0, 'f', 2).arg(logFileInfo.size()/(MAVLINK_MAX_PACKET_LEN+sizeof(quint64))).arg(timelabel)); |
|
} |
|
else |
|
{ |
|
// Load in binary mode |
|
|
|
// Set baud rate if any present |
|
QStringList parts = logFileInfo.baseName().split("_"); |
|
|
|
if (parts.count() > 1) |
|
{ |
|
bool ok; |
|
int rate = parts.last().toInt(&ok); |
|
// 9600 baud to 100 MBit |
|
if (ok && (rate > 9600 && rate < 100000000)) |
|
{ |
|
// Accept this as valid baudrate |
|
binaryBaudRate = rate; |
|
} |
|
} |
|
|
|
int seconds = logFileInfo.size() / (binaryBaudRate / 10); |
|
int minutes = seconds / 60; |
|
int hours = minutes / 60; |
|
seconds -= 60*minutes; |
|
minutes -= 60*hours; |
|
|
|
QString timelabel = tr("%1h:%2m:%3s").arg(hours, 2).arg(minutes, 2).arg(seconds, 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)); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Jumps to the current percentage of the position slider |
|
*/ |
|
void QGCMAVLinkLogPlayer::jumpToSliderVal(int slidervalue) |
|
{ |
|
loopTimer.stop(); |
|
// Set the logfile to the correct percentage and |
|
// align to the timestamp values |
|
int packetCount = logFile.size() / (packetLen + timeLen); |
|
int packetIndex = (packetCount - 1) * (slidervalue / (double)(ui->positionSlider->maximum() - ui->positionSlider->minimum())); |
|
|
|
// Do only accept valid jumps |
|
if (reset(packetIndex)) |
|
{ |
|
if (mavlinkLogFormat) |
|
{ |
|
ui->logStatsLabel->setText(tr("Jumped to packet %1").arg(packetIndex)); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* This function is the "mainloop" of the log player, reading one line |
|
* and adjusting the mainloop timer to read the next line in time. |
|
* It might not perfectly match the timing of the log file, |
|
* but it will never induce a static drift into the log file replay. |
|
* For scientific logging, the use of onboard timestamps and the log |
|
* functionality of the line chart plot is recommended. |
|
*/ |
|
void QGCMAVLinkLogPlayer::logLoop() |
|
{ |
|
if (mavlinkLogFormat) |
|
{ |
|
bool ok; |
|
|
|
// First check initialization |
|
if (startTime == 0) |
|
{ |
|
QByteArray startBytes = logFile.read(timeLen); |
|
|
|
// Check if the correct number of bytes could be read |
|
if (startBytes.length() != timeLen) |
|
{ |
|
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 |
|
startTime = *((quint64*)(startBytes.constData())); |
|
currentStartTime = QGC::groundTimeUsecs(); |
|
ok = true; |
|
|
|
//qDebug() << "START TIME: " << startTime; |
|
|
|
// Check if these bytes could be correctly decoded |
|
if (!ok) |
|
{ |
|
ui->logStatsLabel->setText(tr("Error decoding first timestamp, aborting.")); |
|
MainWindow::instance()->showCriticalMessage(tr("Failed loading MAVLink Logfile"), tr("Could not load initial timestamp from file %1. Is the file corrupted?").arg(logFile.fileName())); |
|
reset(); |
|
return; |
|
} |
|
} |
|
|
|
|
|
// Initialization seems fine, load next chunk |
|
QByteArray chunk = logFile.read(timeLen+packetLen); |
|
QByteArray packet = chunk.mid(0, packetLen); |
|
|
|
// Emit this packet |
|
emit bytesReady(logLink, packet); |
|
|
|
// Check if reached end of file before reading next timestamp |
|
if (chunk.length() < timeLen+packetLen || logFile.atEnd()) |
|
{ |
|
// 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 |
|
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 |
|
{ |
|
// Binary format - read at fixed rate |
|
const int len = 100; |
|
QByteArray chunk = logFile.read(len); |
|
|
|
// Emit this packet |
|
emit bytesReady(logLink, chunk); |
|
|
|
// Check if reached end of file before reading next timestamp |
|
if (chunk.length() < len || logFile.atEnd()) |
|
{ |
|
// Reached end of file |
|
reset(); |
|
|
|
QString status = tr("Reached end of binary log file."); |
|
ui->logStatsLabel->setText(status); |
|
MainWindow::instance()->showStatusMessage(status); |
|
return; |
|
} |
|
} |
|
// Ui update: Only every 20 messages |
|
// to prevent flickering and high CPU load |
|
|
|
// Update status label |
|
// Update progress bar |
|
if (loopCounter % 40 == 0) |
|
{ |
|
QFileInfo logFileInfo(logFile); |
|
int progress = (ui->positionSlider->maximum()-ui->positionSlider->minimum())*(logFile.pos()/static_cast<float>(logFileInfo.size())); |
|
//qDebug() << "Progress:" << progress; |
|
ui->positionSlider->blockSignals(true); |
|
ui->positionSlider->setValue(progress); |
|
ui->positionSlider->blockSignals(false); |
|
} |
|
loopCounter++; |
|
} |
|
|
|
void QGCMAVLinkLogPlayer::changeEvent(QEvent *e) |
|
{ |
|
QWidget::changeEvent(e); |
|
switch (e->type()) |
|
{ |
|
case QEvent::LanguageChange: |
|
ui->retranslateUi(this); |
|
break; |
|
default: |
|
break; |
|
} |
|
}
|
|
|