QGCMAVLinkLogPlayer.cc 22.9 KB
Newer Older
1
#include <QStandardPaths>
2
#include <QtEndian>
3

4
#include "MainWindow.h"
dogmaphobic's avatar
dogmaphobic committed
5
#ifndef __ios__
6
#include "SerialLink.h"
dogmaphobic's avatar
dogmaphobic committed
7
#endif
8
#include "QGCMAVLinkLogPlayer.h"
9
#include "QGC.h"
10
#include "ui_QGCMAVLinkLogPlayer.h"
Don Gagne's avatar
Don Gagne committed
11
#include "QGCApplication.h"
Don Gagne's avatar
Don Gagne committed
12
#include "LinkManager.h"
Don Gagne's avatar
Don Gagne committed
13
#include "QGCFileDialog.h"
14
#include "QGCMessageBox.h"
15 16

QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *parent) :
17
    QWidget(parent),
18 19 20
    playbackStartTime(0),
    logStartTime(0),
    logEndTime(0),
21 22 23 24 25
    accelerationFactor(1.0f),
    mavlink(mavlink),
    logLink(NULL),
    loopCounter(0),
    mavlinkLogFormat(true),
26
    binaryBaudRate(defaultBinaryBaudRate),
lm's avatar
lm committed
27
    isPlaying(false),
28
    currPacketCount(0),
29
    ui(new Ui::QGCMAVLinkLogPlayer)
30
{
Don Gagne's avatar
Don Gagne committed
31 32
    Q_ASSERT(mavlink);
    
33
    ui->setupUi(this);
34
    ui->horizontalLayout->setAlignment(Qt::AlignTop);
35

36 37 38
    // Connect protocol
    connect(this, SIGNAL(bytesReady(LinkInterface*,QByteArray)), mavlink, SLOT(receiveBytes(LinkInterface*,QByteArray)));

39
    // Setup timer
Don Gagne's avatar
Don Gagne committed
40
    connect(&loopTimer, &QTimer::timeout, this, &QGCMAVLinkLogPlayer::logLoop);
41

42
    // Setup buttons
Don Gagne's avatar
Don Gagne committed
43 44 45 46 47 48
    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);
    
49 50
    setAccelerationFactorInt(49);
    ui->speedSlider->setValue(49);
51
    updatePositionSliderUi(0.0);
Lorenz Meier's avatar
Lorenz Meier committed
52 53 54 55

    ui->playButton->setEnabled(false);
    ui->speedSlider->setEnabled(false);
    ui->positionSlider->setEnabled(false);
56 57 58
    ui->speedLabel->setEnabled(false);
    ui->logFileNameLabel->setEnabled(false);
    ui->logStatsLabel->setEnabled(false);
Lorenz Meier's avatar
Lorenz Meier committed
59

60
    // 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
61
    connect(this,  &QGCMAVLinkLogPlayer::logFileEndReached, &loopTimer, &QTimer::stop);
62 63 64 65 66 67 68
}

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

lm's avatar
lm committed
69 70 71 72 73 74 75 76 77 78 79 80
void QGCMAVLinkLogPlayer::playPauseToggle()
{
    if (isPlaying)
    {
        pause();
    }
    else
    {
        play();
    }
}

81 82
void QGCMAVLinkLogPlayer::play()
{
Don Gagne's avatar
Don Gagne committed
83 84 85
    Q_ASSERT(logFile.isOpen());
    
    LinkManager::instance()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay."));
86
    mavlink->suspendLogForReplay(true);
Don Gagne's avatar
Don Gagne committed
87 88 89 90 91 92
    
    // 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
93
    {
Don Gagne's avatar
Don Gagne committed
94 95
        reset();
    }
96

Don Gagne's avatar
Don Gagne committed
97 98 99
    // 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;
100

Don Gagne's avatar
Don Gagne committed
101 102 103 104
    // Start timer
    if (mavlinkLogFormat)
    {
        loopTimer.start(1);
lm's avatar
lm committed
105 106 107
    }
    else
    {
Don Gagne's avatar
Don Gagne committed
108 109 110 111 112 113 114
        // 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);
115
    }
Don Gagne's avatar
Don Gagne committed
116 117
    isPlaying = true;
    ui->playButton->setChecked(true);
Don Gagne's avatar
Don Gagne committed
118
    ui->playButton->setIcon(QIcon(":/res/Pause"));
119 120 121 122
}

void QGCMAVLinkLogPlayer::pause()
{
Don Gagne's avatar
Don Gagne committed
123
    LinkManager::instance()->setConnectionsAllowed();
124
    mavlink->suspendLogForReplay(false);
Don Gagne's avatar
Don Gagne committed
125

126
    loopTimer.stop();
127
    isPlaying = false;
Don Gagne's avatar
Don Gagne committed
128
    ui->playButton->setIcon(QIcon(":/res/Play"));
129
    ui->playButton->setChecked(false);
130 131 132
    ui->selectFileButton->setEnabled(true);
}

133 134 135 136
void QGCMAVLinkLogPlayer::reset()
{
    pause();
    loopCounter = 0;
Don Gagne's avatar
Don Gagne committed
137 138 139
    if (logFile.isOpen()) {
        logFile.reset();
    }
140 141 142 143 144 145 146 147 148 149

    // 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)
150
{
151
    // Reset only for valid values
152
    if (percentage <= 100.0f && percentage >= 0.0f)
lm's avatar
lm committed
153
    {
154
        bool result = true;
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
        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
173

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
            // 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
198
        {
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
            // 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);
215 216
        }

217 218
        // Now update the UI. This is necessary because stop() is called when loading a new logfile

219
        return result;
lm's avatar
lm committed
220 221 222
    }
    else
    {
223 224
        return false;
    }
225 226
}

227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
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
247 248
/// @brief Displays a file dialog to allow the user to select a log file to play back.
void QGCMAVLinkLogPlayer::_selectLogFileForPlayback(void)
Lorenz Meier's avatar
Lorenz Meier committed
249
{
Don Gagne's avatar
Don Gagne committed
250
    // Disallow replay when any links are connected
251
    if (LinkManager::instance()->anyConnectedLinks()) {
252
        QGCMessageBox::information(tr("Log Replay"), tr("You must close all connections prior to replaying a log."));
Don Gagne's avatar
Don Gagne committed
253 254 255
        return;
    }
    
256 257 258 259
    QString logFile = QGCFileDialog::getOpenFileName(
        this,
        tr("Load MAVLink Log File"),
        qgcApp()->mavlinkLogFilesLocation(),
260
        tr("MAVLink Log Files (*.mavlink);;All Files (*)"));
261

Don Gagne's avatar
Don Gagne committed
262 263
    if (!logFile.isEmpty()) {
        loadLogFile(logFile);
264
    }
265 266
}

267 268 269 270 271 272 273 274
/**
 * @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
275 276
    if (f < 0.0f)
    {
277
        accelerationFactor = 1.0f / (-f/2.0f);
lm's avatar
lm committed
278 279 280
    }
    else
    {
281 282 283
        accelerationFactor = 1+(f/2.0f);
    }

284
    // Update timer interval
lm's avatar
lm committed
285 286
    if (!mavlinkLogFormat)
    {
287 288 289 290 291 292 293
        // 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();
294
        loopTimer.start(interval / accelerationFactor);
295 296
    }

297
    ui->speedLabel->setText(tr("Speed: %1X").arg(accelerationFactor, 5, 'f', 2, '0'));
298 299
}

300
bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file)
301
{
302 303
    // Make sure to stop the logging process and reset everything.
    reset();
Don Gagne's avatar
Don Gagne committed
304
    logFile.close();
305

306 307
    // Now load the new file.
    logFile.setFileName(file);
Don Gagne's avatar
Don Gagne committed
308
    if (!logFile.open(QFile::ReadOnly)) {
309
        QGCMessageBox::critical(tr("Log Replay"), tr("The selected file is unreadable. Please make sure that the file %1 is readable or select a different file").arg(file));
Don Gagne's avatar
Don Gagne committed
310
        _playbackError();
311
        return false;
lm's avatar
lm committed
312
    }
Don Gagne's avatar
Don Gagne committed
313 314 315
    
    QFileInfo logFileInfo(file);
    ui->logFileNameLabel->setText(tr("File: %1").arg(logFileInfo.fileName()));
316

Don Gagne's avatar
Don Gagne committed
317 318
    // If there's an existing MAVLinkSimulationLink() being used for an old file,
    // we replace it.
319
    if (logLink) {
320
        LinkManager::instance()->_deleteLink(logLink);
Don Gagne's avatar
Don Gagne committed
321
    }
322 323
    logLink = new MockLink();
    LinkManager::instance()->_addLink(logLink);
324

Don Gagne's avatar
Don Gagne committed
325 326
    // Select if binary or MAVLink log format is used
    mavlinkLogFormat = file.endsWith(".mavlink");
327

Don Gagne's avatar
Don Gagne committed
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
    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;
        }
349

Don Gagne's avatar
Don Gagne committed
350
        if (endtime == starttime) {
351
            QGCMessageBox::critical(tr("Log Replay"), tr("The selected file is corrupt. No valid timestamps were found at the end of the file.").arg(file));
Don Gagne's avatar
Don Gagne committed
352 353 354
            _playbackError();
            return false;
        }
355

Don Gagne's avatar
Don Gagne committed
356 357 358 359
        // Remember the start and end time so we can move around this logfile with the slider.
        logEndTime = endtime;
        logStartTime = starttime;
        logCurrentTime = logStartTime;
360

Don Gagne's avatar
Don Gagne committed
361 362
        // Reset our log file so when we go to read it for the first time, we start at the beginning.
        logFile.reset();
363

Don Gagne's avatar
Don Gagne committed
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
        // 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
387
        {
Don Gagne's avatar
Don Gagne committed
388 389 390 391
            bool ok;
            int rate = parts.last().toInt(&ok);
            // 9600 baud to 100 MBit
            if (ok && (rate > 9600 && rate < 100000000))
lm's avatar
lm committed
392
            {
Don Gagne's avatar
Don Gagne committed
393 394
                // Accept this as valid baudrate
                binaryBaudRate = rate;
395 396
            }
        }
397

Don Gagne's avatar
Don Gagne committed
398 399 400 401 402
        int seconds = logFileInfo.size() / (binaryBaudRate / 10);
        int minutes = seconds / 60;
        int hours = minutes / 60;
        seconds -= 60*minutes;
        minutes -= 60*hours;
403

Don Gagne's avatar
Don Gagne committed
404 405 406
        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));
    }
407

Don Gagne's avatar
Don Gagne committed
408 409 410 411 412 413 414 415 416
    // 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();
417

Don Gagne's avatar
Don Gagne committed
418
    return true;
419 420
}

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
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;
}

438
/**
439 440
 * 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.
441 442
 */
void QGCMAVLinkLogPlayer::jumpToSliderVal(int slidervalue)
443
{
444 445 446 447 448 449 450 451 452 453 454 455
    // 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;
    }
456

457 458
    // Do only valid jumps
    if (jumpToPlaybackLocation(newLocation))
lm's avatar
lm committed
459 460 461
    {
        if (mavlinkLogFormat)
        {
462 463 464 465 466 467 468 469 470 471 472 473
            // 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));
474
        }
475 476 477 478 479 480

        play();
    }
    else
    {
        reset();
481
    }
482
}
483

484 485 486 487 488 489 490 491
/**
 * 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.
 */
492
void QGCMAVLinkLogPlayer::logLoop()
493
{
494 495
    // 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
496 497
    if (mavlinkLogFormat)
    {
498
        // Now parse MAVLink messages, grabbing their timestamps as we go. We stop once we
Bryant Mairs's avatar
Bryant Mairs committed
499 500 501 502
        // 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.
503
        int nextExecutionTime = 0;
Bryant Mairs's avatar
Bryant Mairs committed
504 505 506 507

        // 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.
508
        mavlink_message_t msg;
Bryant Mairs's avatar
Bryant Mairs committed
509 510
        findNextMavlinkMessage(&msg);

511
        while (nextExecutionTime < 3) {
512

513 514
            // 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);
515

516 517
            // Emit this message to our MAVLink parser.
            emit bytesReady(logLink, message);
518

519
            // If we've reached the end of the of the file, make sure we handle that well
Don Gagne's avatar
Don Gagne committed
520 521
            if (logFile.atEnd()) {
                _finishPlayback();
522 523
                return;
            }
524

525 526
            // Run our parser to find the next timestamp and leave us at the start of the next MAVLink message.
            logCurrentTime = findNextMavlinkMessage(&msg);
527

528 529 530 531 532 533
            // 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;
534
        }
535

536 537
        // And schedule the next execution of this function.
        loopTimer.start(nextExecutionTime);
lm's avatar
lm committed
538 539 540
    }
    else
    {
541 542 543 544 545 546 547 548
        // 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
549 550
        if (chunk.length() < len || logFile.atEnd())
        {
Don Gagne's avatar
Don Gagne committed
551
            _finishPlayback();
552 553
            return;
        }
554
    }
555

556 557 558
    // 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
559
    {
560
        QFileInfo logFileInfo(logFile);
561
        updatePositionSliderUi(logFile.pos() / static_cast<float>(logFileInfo.size()));
562
    }
563
    loopCounter++;
564
}
565

566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
/**
 * 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
582
        bool messageFound = mavlink_parse_char(logLink->getMavlinkChannel(), nextByte, msg, &comm);
583 584 585 586 587 588 589 590 591 592 593 594 595 596

        // 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;
}

597 598 599
void QGCMAVLinkLogPlayer::changeEvent(QEvent *e)
{
    QWidget::changeEvent(e);
lm's avatar
lm committed
600 601
    switch (e->type())
    {
602 603 604 605 606 607 608
    case QEvent::LanguageChange:
        ui->retranslateUi(this);
        break;
    default:
        break;
    }
}
609 610 611 612 613 614 615 616 617 618

/**
 * 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);
619
}
Don Gagne's avatar
Don Gagne committed
620 621 622 623 624 625 626 627 628 629 630 631 632 633

/// @brief Called when playback is complete
void QGCMAVLinkLogPlayer::_finishPlayback(void)
{
    pause();
    
    QString status = tr("Flight Data replay complete");
    ui->logStatsLabel->setText(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();
    
634
    mavlink->suspendLogForReplay(false);
Don Gagne's avatar
Don Gagne committed
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
    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);
}