Skip to content
QGCMAVLinkLogPlayer.cc 23.6 KiB
Newer Older
#include <QtEndian>
#include "MainWindow.h"
#include "SerialLink.h"
#include "ui_QGCMAVLinkLogPlayer.h"
Don Gagne's avatar
Don Gagne committed
#include "QGCApplication.h"
Don Gagne's avatar
Don Gagne committed
#include "LinkManager.h"
Don Gagne's avatar
Don Gagne committed
#include "QGCFileDialog.h"

QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *parent) :
    QWidget(parent),
    playbackStartTime(0),
    logStartTime(0),
    logEndTime(0),
    accelerationFactor(1.0f),
    mavlink(mavlink),
    logLink(NULL),
    loopCounter(0),
    mavlinkLogFormat(true),
    binaryBaudRate(defaultBinaryBaudRate),
lm's avatar
lm committed
    isPlaying(false),
    ui(new Ui::QGCMAVLinkLogPlayer)
Don Gagne's avatar
Don Gagne committed
    Q_ASSERT(mavlink);
    
    ui->horizontalLayout->setAlignment(Qt::AlignTop);
    // Connect protocol
    connect(this, SIGNAL(bytesReady(LinkInterface*,QByteArray)), mavlink, SLOT(receiveBytes(LinkInterface*,QByteArray)));

Don Gagne's avatar
Don Gagne committed
    connect(&loopTimer, &QTimer::timeout, this, &QGCMAVLinkLogPlayer::logLoop);
Don Gagne's avatar
Don Gagne committed
    connect(ui->selectFileButton, &QPushButton::clicked, this, &QGCMAVLinkLogPlayer::_selectLogFileForPlayback);
    connect(ui->playButton, &QPushButton::clicked, this, &QGCMAVLinkLogPlayer::playPauseToggle);
    connect(ui->speedSlider, &QSlider::valueChanged, this, &QGCMAVLinkLogPlayer::setAccelerationFactorInt);
    connect(ui->positionSlider, &QSlider::valueChanged, this, &QGCMAVLinkLogPlayer::jumpToSliderVal);
    connect(ui->positionSlider, &QSlider::sliderPressed, this, &QGCMAVLinkLogPlayer::pause);
    
    // We use this to queue the signal over to mavlink. This way it will be behind any remaining
    // bytesReady signals in the queue.
    connect(this, &QGCMAVLinkLogPlayer::suspendLogForReplay, mavlink, &MAVLinkProtocol::suspendLogForReplay);
    setAccelerationFactorInt(49);
    ui->speedSlider->setValue(49);
    updatePositionSliderUi(0.0);
Lorenz Meier's avatar
Lorenz Meier committed

    ui->playButton->setEnabled(false);
    ui->speedSlider->setEnabled(false);
    ui->positionSlider->setEnabled(false);
    ui->speedLabel->setEnabled(false);
    ui->logFileNameLabel->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.
Don Gagne's avatar
Don Gagne committed
    connect(this,  &QGCMAVLinkLogPlayer::logFileEndReached, &loopTimer, &QTimer::stop);
}

QGCMAVLinkLogPlayer::~QGCMAVLinkLogPlayer()
{
    delete ui;
}

lm's avatar
lm committed
void QGCMAVLinkLogPlayer::playPauseToggle()
{
    if (isPlaying)
    {
        pause();
    }
    else
    {
        play();
    }
}

Don Gagne's avatar
Don Gagne committed
    Q_ASSERT(logFile.isOpen());
    
    LinkManager::instance()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay."));
    emit suspendLogForReplay(true);
    
    // Disable the log file selector button
    ui->selectFileButton->setEnabled(false);

    // 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())
lm's avatar
lm committed
    {
Don Gagne's avatar
Don Gagne committed
        reset();
    }
Don Gagne's avatar
Don Gagne committed
    // 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;
Don Gagne's avatar
Don Gagne committed
    // Start timer
    if (mavlinkLogFormat)
    {
        loopTimer.start(1);
lm's avatar
lm committed
    }
    else
    {
Don Gagne's avatar
Don Gagne committed
        // 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);
Don Gagne's avatar
Don Gagne committed
    isPlaying = true;
    ui->playButton->setChecked(true);
    ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-pause.svg"));
Don Gagne's avatar
Don Gagne committed
    LinkManager::instance()->setConnectionsAllowed();
    emit suspendLogForReplay(false);

    ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-start.svg"));
    ui->playButton->setChecked(false);
    ui->selectFileButton->setEnabled(true);
}

void QGCMAVLinkLogPlayer::reset()
{
    pause();
    loopCounter = 0;
Don Gagne's avatar
Don Gagne committed
    if (logFile.isOpen()) {
        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
    if (percentage <= 100.0f && percentage >= 0.0f)
lm's avatar
lm committed
    {
        bool result = true;
        if (mavlinkLogFormat)
        {
            // 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);
lm's avatar
lm committed

            // 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
lm's avatar
lm committed
        {
            // If we're working with a non-timestamped file, we just jump to that percentage of the file,
            // align to the next MAVLink message and roll with it. No reason to do anything more complicated.
            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;
            findNextMavlinkMessage(&dummy);
        // Now update the UI. This is necessary because stop() is called when loading a new logfile

        return result;
lm's avatar
lm committed
    }
    else
    {
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);
}

Don Gagne's avatar
Don Gagne committed
/// @brief Displays a file dialog to allow the user to select a log file to play back.
void QGCMAVLinkLogPlayer::_selectLogFileForPlayback(void)
Don Gagne's avatar
Don Gagne committed
    // Disallow replay when any links are connected
    
    bool foundConnection = false;
    LinkManager* linkMgr = LinkManager::instance();
    QList<LinkInterface*> links = linkMgr->getLinks();
    foreach(LinkInterface* link, links) {
        if (link->isConnected()) {
            foundConnection = true;
            break;
        }
Don Gagne's avatar
Don Gagne committed
    
    if (foundConnection) {
        MainWindow::instance()->showInfoMessage(tr("Log Replay"), tr("You must close all connections prior to replaying a log."));
        return;
    }
    
Don Gagne's avatar
Don Gagne committed
    QString logFile = QGCFileDialog::getOpenFileName(this,
Don Gagne's avatar
Don Gagne committed
                                                    tr("Specify MAVLink log file name to replay"),
                                                    qgcApp()->mavlinkLogFilesLocation(),
                                                    tr("MAVLink or Binary Logfile (*.mavlink *.bin *.log)"));
    
    if (!logFile.isEmpty()) {
        loadLogFile(logFile);
/**
 * @param factor 0: 0.01X, 50: 1.0X, 100: 100.0X
 */
void QGCMAVLinkLogPlayer::setAccelerationFactorInt(int factor)
{
    float f = factor+1.0f;
    f -= 50.0f;

lm's avatar
lm committed
    if (f < 0.0f)
    {
        accelerationFactor = 1.0f / (-f/2.0f);
lm's avatar
lm committed
    }
    else
    {
        accelerationFactor = 1+(f/2.0f);
    }

    // Update timer interval
lm's avatar
lm committed
    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);
    ui->speedLabel->setText(tr("Speed: %1X").arg(accelerationFactor, 5, 'f', 2, '0'));
bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file)
    // Make sure to stop the logging process and reset everything.
    reset();
Don Gagne's avatar
Don Gagne committed
    logFile.close();
    // Now load the new file.
    logFile.setFileName(file);
Don Gagne's avatar
Don Gagne committed
    if (!logFile.open(QFile::ReadOnly)) {
        MainWindow::instance()->showCriticalMessage(tr("The selected file is unreadable"), tr("Please make sure that the file %1 is readable or select a different file").arg(file));
        _playbackError();
        return false;
lm's avatar
lm committed
    }
Don Gagne's avatar
Don Gagne committed
    
    QFileInfo logFileInfo(file);
    ui->logFileNameLabel->setText(tr("File: %1").arg(logFileInfo.fileName()));
Don Gagne's avatar
Don Gagne committed
    // If there's an existing MAVLinkSimulationLink() being used for an old file,
    // we replace it.
    if (logLink)
    {
        LinkManager::instance()->disconnectLink(logLink);
        logLink->deleteLater();
    }
    logLink = new MAVLinkSimulationLink("");
Don Gagne's avatar
Don Gagne committed
    // Select if binary or MAVLink log format is used
    mavlinkLogFormat = file.endsWith(".mavlink");
Don Gagne's avatar
Don Gagne committed
    if (mavlinkLogFormat)
    {
        // Get the first timestamp from the logfile
        // This should be a big-endian uint64.
        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;
        }
Don Gagne's avatar
Don Gagne committed
        if (endtime == starttime) {
            MainWindow::instance()->showCriticalMessage(tr("The selected file is corrupt"), tr("No valid timestamps were found at the end of the file.").arg(file));
            _playbackError();
            return false;
        }
Don Gagne's avatar
Don Gagne committed
        // Remember the start and end time so we can move around this logfile with the slider.
        logEndTime = endtime;
        logStartTime = starttime;
        logCurrentTime = logStartTime;
Don Gagne's avatar
Don Gagne committed
        // Reset our log file so when we go to read it for the first time, we start at the beginning.
        logFile.reset();
Don Gagne's avatar
Don Gagne committed
        // Calculate the runtime in hours:minutes:seconds
        // WARNING: Order matters in this computation
        quint32 seconds = (endtime - starttime)/1000000;
        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);
        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));
    }
    else
    {
        // 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. Otherwise we default to 57600.
        QStringList parts = logFileInfo.baseName().split("_");
        binaryBaudRate = defaultBinaryBaudRate;
        if (parts.count() > 1)
lm's avatar
lm committed
        {
Don Gagne's avatar
Don Gagne committed
            bool ok;
            int rate = parts.last().toInt(&ok);
            // 9600 baud to 100 MBit
            if (ok && (rate > 9600 && rate < 100000000))
lm's avatar
lm committed
            {
Don Gagne's avatar
Don Gagne committed
                // Accept this as valid baudrate
                binaryBaudRate = rate;
Don Gagne's avatar
Don Gagne committed
        int seconds = logFileInfo.size() / (binaryBaudRate / 10);
        int minutes = seconds / 60;
        int hours = minutes / 60;
        seconds -= 60*minutes;
        minutes -= 60*hours;
Don Gagne's avatar
Don Gagne committed
        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));
    }
Don Gagne's avatar
Don Gagne committed
    // Enable controls
    ui->playButton->setEnabled(true);
    ui->speedSlider->setEnabled(true);
    ui->positionSlider->setEnabled(true);
    ui->speedLabel->setEnabled(true);
    ui->logFileNameLabel->setEnabled(true);
    ui->logStatsLabel->setEnabled(true);
    
    play();
Don Gagne's avatar
Don Gagne committed
    return true;
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. 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)
    // Determine what percentage through the file we should be (time or packet number depending).
    float newLocation = slidervalue / (float)(ui->positionSlider->maximum() - ui->positionSlider->minimum());

    // And clamp our calculated values to the valid range of [0,100]
    if (newLocation > 100.0f)
    {
        newLocation = 100.0f;
    }
    if (newLocation < 0.0f)
    {
        newLocation = 0.0f;
    }
    // Do only valid jumps
    if (jumpToPlaybackLocation(newLocation))
lm's avatar
lm committed
    {
        if (mavlinkLogFormat)
        {
            // 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));
/**
 * 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 we have a file with timestamps, try and pace this out following the time differences
    // between the timestamps and the current playback speed.
lm's avatar
lm committed
    if (mavlinkLogFormat)
    {
        // Now parse MAVLink messages, grabbing their timestamps as we go. We stop once we
Bryant Mairs's avatar
Bryant Mairs committed
        // have at least 3ms until the next one. 

        // We track what the next execution time should be in milliseconds, which we use to set
        // the next timer interrupt.
        int nextExecutionTime = 0;
Bryant Mairs's avatar
Bryant Mairs committed

        // We use the `findNextMavlinkMessage()` function to scan ahead for MAVLink messages. This
        // is necessary because we don't know how big each MAVLink message is until we finish parsing
        // one, and since we only output arrays of bytes, we need to know the size of that array.
        mavlink_message_t msg;
Bryant Mairs's avatar
Bryant Mairs committed
        findNextMavlinkMessage(&msg);

        while (nextExecutionTime < 3) {
            // Now we're sitting at the start of a MAVLink message, so read it all into a byte array for feeding to our parser.
            QByteArray message = logFile.read(msg.len + MAVLINK_NUM_NON_PAYLOAD_BYTES);
            // Emit this message to our MAVLink parser.
            emit bytesReady(logLink, message);
            // If we've reached the end of the of the file, make sure we handle that well
Don Gagne's avatar
Don Gagne committed
            if (logFile.atEnd()) {
                _finishPlayback();
            // Run our parser to find the next timestamp and leave us at the start of the next MAVLink message.
            logCurrentTime = findNextMavlinkMessage(&msg);
            // Calculate how long we should wait in real time until parsing this message.
            // We pace ourselves relative to the start time of playback to fix any drift (initially set in play())
            qint64 timediff = (logCurrentTime - logStartTime) / accelerationFactor;
            quint64 desiredPacedTime = playbackStartTime + ((quint64)timediff) / 1000;
            quint64 currentTime = (quint64)QDateTime::currentMSecsSinceEpoch();
            nextExecutionTime = desiredPacedTime - currentTime;
        // And schedule the next execution of this function.
        loopTimer.start(nextExecutionTime);
lm's avatar
lm committed
    }
    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
lm's avatar
lm committed
        if (chunk.length() < len || logFile.atEnd())
        {
Don Gagne's avatar
Don Gagne committed
            _finishPlayback();
    // Update the UI every 2^5=32 times, or when there isn't much data to be played back.
    // Reduces flickering and minimizes CPU load.
    if ((loopCounter & 0x1F) == 0 || currPacketCount < 2000)
lm's avatar
lm committed
    {
        QFileInfo logFileInfo(logFile);
        updatePositionSliderUi(logFile.pos() / static_cast<float>(logFileInfo.size()));
    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)
{
    QWidget::changeEvent(e);
lm's avatar
lm committed
    switch (e->type())
    {
    case QEvent::LanguageChange:
        ui->retranslateUi(this);
        break;
    default:
        break;
    }
}

/**
 * Implement paintEvent() so that stylesheets work for our custom widget.
 */
void QGCMAVLinkLogPlayer::paintEvent(QPaintEvent *)
{
    QStyleOption opt;
    opt.init(this);
    QPainter p(this);
    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
Don Gagne's avatar
Don Gagne committed

/// @brief Called when playback is complete
void QGCMAVLinkLogPlayer::_finishPlayback(void)
{
    pause();
    
    QString status = tr("Flight Data replay complete");
    ui->logStatsLabel->setText(status);
    MainWindow::instance()->showStatusMessage(status);
    
    // 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();
    
    emit suspendLogForReplay(false);
    LinkManager::instance()->setConnectionsAllowed();
}

/// @brief Called when an error occurs during playback to reset playback system state.
void QGCMAVLinkLogPlayer::_playbackError(void)
{
    pause();
    
    logFile.close();
    logFile.setFileName("");
    
    ui->logFileNameLabel->setText(tr("No Flight Data selected"));    
    
    // Disable playback controls
    ui->playButton->setEnabled(false);
    ui->speedSlider->setEnabled(false);
    ui->positionSlider->setEnabled(false);
    ui->speedLabel->setEnabled(false);
    ui->logFileNameLabel->setEnabled(false);
    ui->logStatsLabel->setEnabled(false);
}