LogReplayLink.cc 18.2 KB
Newer Older
1
2
/****************************************************************************
 *
Gus Grubba's avatar
Gus Grubba committed
3
 * (c) 2009-2020 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4
5
6
7
8
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/
Don Gagne's avatar
Don Gagne committed
9
10
11
12


#include "LogReplayLink.h"
#include "LinkManager.h"
13
#include "QGCApplication.h"
Don Gagne's avatar
Don Gagne committed
14
15

#include <QFileInfo>
DonLakeFlyer's avatar
DonLakeFlyer committed
16
#include <QtEndian>
Don Gagne's avatar
   
Don Gagne committed
17
#include <QSignalSpy>
Don Gagne's avatar
Don Gagne committed
18
19
20

const char*  LogReplayLinkConfiguration::_logFilenameKey = "logFilename";

DonLakeFlyer's avatar
DonLakeFlyer committed
21
LogReplayLinkConfiguration::LogReplayLinkConfiguration(const QString& name)
Don Gagne's avatar
   
Don Gagne committed
22
    : LinkConfiguration(name)
Don Gagne's avatar
Don Gagne committed
23
24
25
26
{
    
}

DonLakeFlyer's avatar
DonLakeFlyer committed
27
LogReplayLinkConfiguration::LogReplayLinkConfiguration(LogReplayLinkConfiguration* copy)
Don Gagne's avatar
   
Don Gagne committed
28
    : LinkConfiguration(copy)
Don Gagne's avatar
Don Gagne committed
29
30
31
32
33
34
35
{
    _logFilename = copy->logFilename();
}

void LogReplayLinkConfiguration::copyFrom(LinkConfiguration *source)
{
    LinkConfiguration::copyFrom(source);
36
    auto* ssource = qobject_cast<LogReplayLinkConfiguration*>(source);
DonLakeFlyer's avatar
DonLakeFlyer committed
37
38
39
40
41
    if (ssource) {
        _logFilename = ssource->logFilename();
    } else {
        qWarning() << "Internal error";
    }
Don Gagne's avatar
Don Gagne committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
}

void LogReplayLinkConfiguration::saveSettings(QSettings& settings, const QString& root)
{
    settings.beginGroup(root);
    settings.setValue(_logFilenameKey, _logFilename);
    settings.endGroup();
}

void LogReplayLinkConfiguration::loadSettings(QSettings& settings, const QString& root)
{
    settings.beginGroup(root);
    _logFilename = settings.value(_logFilenameKey, "").toString();
    settings.endGroup();
}

QString LogReplayLinkConfiguration::logFilenameShort(void)
{
    QFileInfo fi(_logFilename);
    return fi.fileName();
}

64
LogReplayLink::LogReplayLink(SharedLinkConfigurationPtr& config)
Don Gagne's avatar
   
Don Gagne committed
65
    : LinkInterface     (config)
66
    , _logReplayConfig  (qobject_cast<LogReplayLinkConfiguration*>(config.get()))
Don Gagne's avatar
   
Don Gagne committed
67
68
    , _connected        (false)
    , _playbackSpeed    (1)
Don Gagne's avatar
Don Gagne committed
69
{
DonLakeFlyer's avatar
DonLakeFlyer committed
70
71
72
    if (!_logReplayConfig) {
        qWarning() << "Internal error";
    }
73
74

    _errorTitle = tr("Log Replay Error");
Don Gagne's avatar
Don Gagne committed
75
76
77
    
    _readTickTimer.moveToThread(this);
    
Don Gagne's avatar
   
Don Gagne committed
78
79
80
81
    QObject::connect(&_readTickTimer, &QTimer::timeout,                 this, &LogReplayLink::_readNextLogEntry);
    QObject::connect(this, &LogReplayLink::_playOnThread,               this, &LogReplayLink::_play);
    QObject::connect(this, &LogReplayLink::_pauseOnThread,              this, &LogReplayLink::_pause);
    QObject::connect(this, &LogReplayLink::_setPlaybackSpeedOnThread,   this, &LogReplayLink::_setPlaybackSpeed);
Don Gagne's avatar
Don Gagne committed
82
83
84
85
86
87
    
    moveToThread(this);
}

LogReplayLink::~LogReplayLink(void)
{
88
    disconnect();
Don Gagne's avatar
Don Gagne committed
89
90
91
92
93
}

bool LogReplayLink::_connect(void)
{
    // Disallow replay when any links are connected
94
    if (qgcApp()->toolbox()->multiVehicleManager()->activeVehicle()) {
95
        emit communicationError(_errorTitle, tr("You must close all connections prior to replaying a log."));
Don Gagne's avatar
Don Gagne committed
96
97
98
99
100
101
102
103
104
105
106
        return false;
    }

    if (isRunning()) {
        quit();
        wait();
    }
    start(HighPriority);
    return true;
}

107
void LogReplayLink::disconnect(void)
Don Gagne's avatar
Don Gagne committed
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
{
    if (_connected) {
        quit();
        wait();
        _connected = false;
        emit disconnected();
    }
}

void LogReplayLink::run(void)
{
    // Load the log file
    if (!_loadLogFile()) {
        return;
    }
    
    _connected = true;
    emit connected();
    
    // Start playback
    _play();

    // Run normal event loop until exit
    exec();
132
133
    
    _readTickTimer.stop();
Don Gagne's avatar
Don Gagne committed
134
135
136
137
138
139
140
141
142
}

void LogReplayLink::_replayError(const QString& errorMsg)
{
    qDebug() << _errorTitle << errorMsg;
    emit communicationError(_errorTitle, errorMsg);
}

/// Since this is log replay, we just drops writes on the floor
143
void LogReplayLink::_writeBytes(const QByteArray bytes)
Don Gagne's avatar
Don Gagne committed
144
145
146
147
148
149
150
151
{
    Q_UNUSED(bytes);
}

/// Parses a BigEndian quint64 timestamp
/// @return A Unix timestamp in microseconds UTC for found message or 0 if parsing failed
quint64 LogReplayLink::_parseTimestamp(const QByteArray& bytes)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
152
    quint64 timestamp = qFromBigEndian(*((quint64*)(bytes.constData())));
Don Gagne's avatar
Don Gagne committed
153
154
155
156
157
    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) {
DonLakeFlyer's avatar
DonLakeFlyer committed
158
        timestamp = qbswap(timestamp);
Don Gagne's avatar
Don Gagne committed
159
160
161
162
163
    }
    
    return timestamp;
}

Don Gagne's avatar
Don Gagne committed
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/// Reads the next mavlink message from the log
///     @param bytes[output] Bytes for mavlink message
/// @return Unix timestamp in microseconds UTC for NEXT mavlink message or 0 if no message found
quint64 LogReplayLink::_readNextMavlinkMessage(QByteArray& bytes)
{
    char                nextByte;
    mavlink_status_t    status;

    bytes.clear();

    while (_logFile.getChar(&nextByte)) { // Loop over every byte
        mavlink_message_t message;
        bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, &message, &status);

        if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
            // This is the possible beginning of a mavlink message, clear any partial bytes
            bytes.clear();
        }
        bytes.append(nextByte);

        if (messageFound) {
            // Return the timestamp for the next message
            QByteArray rawTime = _logFile.read(cbTimestamp);
            return _parseTimestamp(rawTime);
        }
    }

    return 0;
}

Ricardo de Almeida Gonzaga's avatar
Ricardo de Almeida Gonzaga committed
194
/// Seeks to the beginning of the next successfully parsed mavlink message in the log file.
Don Gagne's avatar
Don Gagne committed
195
196
197
198
///     @param nextMsg[output] Parsed next message that was found
/// @return A Unix timestamp in microseconds UTC for found message or 0 if parsing failed
quint64 LogReplayLink::_seekToNextMavlinkMessage(mavlink_message_t* nextMsg)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
199
200
201
202
    char                nextByte;
    mavlink_status_t    status;
    qint64              messageStartPos = -1;

Don Gagne's avatar
   
Don Gagne committed
203
204
205
    mavlink_reset_channel_status(_mavlinkChannel);

    while (_logFile.getChar(&nextByte)) {
Don Gagne's avatar
Don Gagne committed
206
        bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, nextMsg, &status);
DonLakeFlyer's avatar
DonLakeFlyer committed
207
208
209
210
211

        if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
            // This is the possible beginning of a mavlink message
            messageStartPos = _logFile.pos() - 1;
        }
Don Gagne's avatar
Don Gagne committed
212
213
214
        
        // 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.
DonLakeFlyer's avatar
DonLakeFlyer committed
215
216
        if (messageFound && messageStartPos != -1) {
            _logFile.seek(messageStartPos - cbTimestamp);
Don Gagne's avatar
Don Gagne committed
217
218
219
220
221
222
223
224
            QByteArray rawTime = _logFile.read(cbTimestamp);
            return _parseTimestamp(rawTime);
        }
    }
    
    return 0;
}

Don Gagne's avatar
   
Don Gagne committed
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
quint64 LogReplayLink::_findLastTimestamp(void)
{
    char                nextByte;
    mavlink_status_t    status;
    quint64             lastTimestamp = 0;
    mavlink_message_t   msg;

    // We read through the entire file looking for the last good timestamp. This can be somewhat slow, but trying to work from the
    // end of the file can be way slower due to all the seeking back and forth required. So instead we take the simple reliable approach.

    _logFile.reset();
    mavlink_reset_channel_status(_mavlinkChannel);

    while (_logFile.bytesAvailable() > cbTimestamp) {
        lastTimestamp = _parseTimestamp(_logFile.read(cbTimestamp));

        bool endOfMessage = false;
        while (!endOfMessage && _logFile.getChar(&nextByte)) {
            endOfMessage = mavlink_parse_char(_mavlinkChannel, nextByte, &msg, &status);
        }
    }

    return lastTimestamp;
}

Don Gagne's avatar
Don Gagne committed
250
251
252
bool LogReplayLink::_loadLogFile(void)
{
    QString errorMsg;
253
    QString logFilename = _logReplayConfig->logFilename();
Don Gagne's avatar
Don Gagne committed
254
255
    QFileInfo logFileInfo;
    int logDurationSecondsTotal;
Don Gagne's avatar
   
Don Gagne committed
256
257
258
    quint64 startTimeUSecs;
    quint64 endTimeUSecs;

Don Gagne's avatar
Don Gagne committed
259
    if (_logFile.isOpen()) {
260
        errorMsg = tr("Attempt to load new log while log being played");
Don Gagne's avatar
Don Gagne committed
261
262
263
264
265
        goto Error;
    }
    
    _logFile.setFileName(logFilename);
    if (!_logFile.open(QFile::ReadOnly)) {
266
        errorMsg = tr("Unable to open log file: '%1', error: %2").arg(logFilename).arg(_logFile.errorString());
Don Gagne's avatar
Don Gagne committed
267
268
269
270
271
        goto Error;
    }
    logFileInfo.setFile(logFilename);
    _logFileSize = logFileInfo.size();
    
Don Gagne's avatar
   
Don Gagne committed
272
273
    startTimeUSecs = _parseTimestamp(_logFile.read(cbTimestamp));
    endTimeUSecs = _findLastTimestamp();
DonLakeFlyer's avatar
DonLakeFlyer committed
274

Don Gagne's avatar
   
Don Gagne committed
275
276
277
    if (endTimeUSecs <= startTimeUSecs) {
        errorMsg = tr("The log file '%1' is corrupt or empty.").arg(logFilename);
        goto Error;
Don Gagne's avatar
Don Gagne committed
278
    }
Don Gagne's avatar
   
Don Gagne committed
279
280
281
282
283
284
285
286
287
288
289

    // Remember the start and end time so we can move around this _logFile with the slider.
    _logEndTimeUSecs = endTimeUSecs;
    _logStartTimeUSecs = startTimeUSecs;
    _logDurationUSecs = endTimeUSecs - startTimeUSecs;
    _logCurrentTimeUSecs = startTimeUSecs;

    // Reset our log file so when we go to read it for the first time, we start at the beginning.
    _logFile.reset();

    logDurationSecondsTotal = (_logDurationUSecs) / 1000000;
Don Gagne's avatar
Don Gagne committed
290
    
Don Gagne's avatar
   
Don Gagne committed
291
    emit logFileStats(logDurationSecondsTotal);
Don Gagne's avatar
Don Gagne committed
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
    
    return true;
    
Error:
    if (_logFile.isOpen()) {
        _logFile.close();
    }
    _replayError(errorMsg);
    return false;
}

/// This function will read the next available log entry. It will then start
/// the _readTickTimer timer to read the new log entry at the appropriate 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.
void LogReplayLink::_readNextLogEntry(void)
{
Don Gagne's avatar
Don Gagne committed
309
310
    QByteArray bytes;

Don Gagne's avatar
   
Don Gagne committed
311
312
    // Now parse MAVLink messages, grabbing their timestamps as we go. We stop once we
    // have at least 3ms until the next one.
DonLakeFlyer's avatar
DonLakeFlyer committed
313

Don Gagne's avatar
   
Don Gagne committed
314
315
316
    // We track what the next execution time should be in milliseconds, which we use to set
    // the next timer interrupt.
    int timeToNextExecutionMSecs = 0;
DonLakeFlyer's avatar
DonLakeFlyer committed
317

Don Gagne's avatar
   
Don Gagne committed
318
319
320
321
322
323
324
    while (timeToNextExecutionMSecs < 3) {
        // Read the next mavlink message from the log
        qint64 nextTimeUSecs = _readNextMavlinkMessage(bytes);
        emit bytesReceived(this, bytes);
        emit playbackPercentCompleteChanged(((float)(_logCurrentTimeUSecs - _logStartTimeUSecs) / (float)_logDurationUSecs) * 100);

        if (_logFile.atEnd()) {
Don Gagne's avatar
Don Gagne committed
325
326
327
            _finishPlayback();
            return;
        }
Don Gagne's avatar
   
Don Gagne committed
328
329
330
331
332
333
334
335
336
337
338

        _logCurrentTimeUSecs = nextTimeUSecs;

        // 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())

        quint64 currentTimeMSecs =                  (quint64)QDateTime::currentMSecsSinceEpoch();
        quint64 desiredPlayheadMovementTimeMSecs =  ((_logCurrentTimeUSecs - _playbackStartLogTimeUSecs) / 1000) / _playbackSpeed;
        quint64 desiredCurrentTimeMSecs =           _playbackStartTimeMSecs + desiredPlayheadMovementTimeMSecs;

        timeToNextExecutionMSecs = desiredCurrentTimeMSecs - currentTimeMSecs;
Don Gagne's avatar
Don Gagne committed
339
    }
Don Gagne's avatar
   
Don Gagne committed
340
341
342
343
344

    _signalCurrentLogTimeSecs();

    // And schedule the next execution of this function.
    _readTickTimer.start(timeToNextExecutionMSecs);
Don Gagne's avatar
Don Gagne committed
345
346
347
348
}

void LogReplayLink::_play(void)
{
349
    qgcApp()->toolbox()->linkManager()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay."));
Don Gagne's avatar
Don Gagne committed
350
#ifndef __mobile__
351
    qgcApp()->toolbox()->mavlinkProtocol()->suspendLogForReplay(true);
Don Gagne's avatar
Don Gagne committed
352
#endif
Don Gagne's avatar
Don Gagne committed
353
354
355
356
357
358
    
    // 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()) {
        _resetPlaybackToBeginning();
    }
    
Don Gagne's avatar
   
Don Gagne committed
359
360
361
    _playbackStartTimeMSecs = (quint64)QDateTime::currentMSecsSinceEpoch();
    _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
    _readTickTimer.start(1);
Don Gagne's avatar
Don Gagne committed
362
363
364
365
366
367
    
    emit playbackStarted();
}

void LogReplayLink::_pause(void)
{
368
    qgcApp()->toolbox()->linkManager()->setConnectionsAllowed();
Don Gagne's avatar
Don Gagne committed
369
#ifndef __mobile__
370
    qgcApp()->toolbox()->mavlinkProtocol()->suspendLogForReplay(false);
Don Gagne's avatar
Don Gagne committed
371
#endif
Don Gagne's avatar
Don Gagne committed
372
373
374
375
376
377
378
379
380
381
382
383
384
385
    
    _readTickTimer.stop();
    
    emit playbackPaused();
}

void LogReplayLink::_resetPlaybackToBeginning(void)
{
    if (_logFile.isOpen()) {
        _logFile.reset();
    }
    
    // And since we haven't starting playback, clear the time of initial playback and the current timestamp.
    _playbackStartTimeMSecs = 0;
Don Gagne's avatar
   
Don Gagne committed
386
    _playbackStartLogTimeUSecs = 0;
Don Gagne's avatar
Don Gagne committed
387
388
389
    _logCurrentTimeUSecs = _logStartTimeUSecs;
}

Don Gagne's avatar
   
Don Gagne committed
390
void LogReplayLink::movePlayhead(qreal percentComplete)
Don Gagne's avatar
Don Gagne committed
391
392
{
    if (isPlaying()) {
Don Gagne's avatar
   
Don Gagne committed
393
        _pauseOnThread();
Don Gagne's avatar
   
Don Gagne committed
394
        QSignalSpy waitForPause(this, SIGNAL(playbackPaused()));
Don Gagne's avatar
   
Don Gagne committed
395
396
397
398
        waitForPause.wait();
        if (_readTickTimer.isActive()) {
            return;
        }
Don Gagne's avatar
Don Gagne committed
399
400
    }

Don Gagne's avatar
   
Don Gagne committed
401
402
403
404
405
    if (percentComplete < 0) {
        percentComplete = 0;
    }
    if (percentComplete > 100) {
        percentComplete = 100;
Don Gagne's avatar
Don Gagne committed
406
407
    }
    
Don Gagne's avatar
   
Don Gagne committed
408
    qreal percentCompleteMult = percentComplete / 100.0;
Don Gagne's avatar
Don Gagne committed
409
    
Don Gagne's avatar
   
Don Gagne committed
410
411
412
413
414
415
416
417
    // 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)(percentCompleteMult * (qreal)_logFile.size());

    // Now seek to the appropriate position, failing gracefully if we can't.
    if (!_logFile.seek(newFilePos)) {
        _replayError(tr("Unable to seek to new position"));
        return;
Don Gagne's avatar
Don Gagne committed
418
    }
Don Gagne's avatar
   
Don Gagne committed
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448

    // But we do align to the next MAVLink message for consistency.
    mavlink_message_t dummy;
    _logCurrentTimeUSecs = _seekToNextMavlinkMessage(&dummy);

    // Now calculate the current file location based on time.
    qreal newRelativeTimeUSecs = (qreal)(_logCurrentTimeUSecs - _logStartTimeUSecs);

    // Calculate the effective baud rate of the file in bytes/s.
    qreal baudRate = _logFile.size() / (qreal)_logDurationUSecs / 1e6;

    // And the desired time is:
    qreal desiredTimeUSecs = percentCompleteMult * _logDurationUSecs;

    // And now jump the necessary number of bytes in the proper direction
    qint64 offset = (newRelativeTimeUSecs - desiredTimeUSecs) * baudRate;
    if (!_logFile.seek(_logFile.pos() + offset)) {
        _replayError(tr("Unable to seek to new position"));
        return;
    }

    // And scan until we reach the start of a MAVLink message. We make sure to record this timestamp for
    // smooth jumping around the file.
    _logCurrentTimeUSecs = _seekToNextMavlinkMessage(&dummy);
    _signalCurrentLogTimeSecs();

    // Now update the UI with our actual final position.
    newRelativeTimeUSecs = (qreal)(_logCurrentTimeUSecs - _logStartTimeUSecs);
    percentComplete = (newRelativeTimeUSecs / _logDurationUSecs) * 100;
    emit playbackPercentCompleteChanged(percentComplete);
Don Gagne's avatar
Don Gagne committed
449
450
}

Don Gagne's avatar
   
Don Gagne committed
451
void LogReplayLink::_setPlaybackSpeed(qreal playbackSpeed)
Don Gagne's avatar
Don Gagne committed
452
{
Don Gagne's avatar
   
Don Gagne committed
453
    _playbackSpeed = playbackSpeed;
Don Gagne's avatar
Don Gagne committed
454
    
Don Gagne's avatar
   
Don Gagne committed
455
456
457
458
    // Let _readNextLogEntry update to correct speed
    _playbackStartTimeMSecs = (quint64)QDateTime::currentMSecsSinceEpoch();
    _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
    _readTickTimer.start(1);
Don Gagne's avatar
Don Gagne committed
459
460
461
462
463
464
465
466
467
468
}

/// @brief Called when playback is complete
void LogReplayLink::_finishPlayback(void)
{
    _pause();
    
    emit playbackAtEnd();
}

Don Gagne's avatar
   
Don Gagne committed
469
void LogReplayLink::_signalCurrentLogTimeSecs(void)
Don Gagne's avatar
Don Gagne committed
470
{
Don Gagne's avatar
   
Don Gagne committed
471
    emit currentLogTimeSecs((_logCurrentTimeUSecs - _logStartTimeUSecs) / 1000000);
Don Gagne's avatar
Don Gagne committed
472
}
Don Gagne's avatar
   
Don Gagne committed
473

Don Gagne's avatar
   
Don Gagne committed
474
475
476
477
478
LogReplayLinkController::LogReplayLinkController(void)
    : _link             (nullptr)
    , _isPlaying        (false)
    , _percentComplete  (0)
    , _playheadSecs     (0)
Don Gagne's avatar
   
Don Gagne committed
479
    , _playbackSpeed    (1)
Don Gagne's avatar
   
Don Gagne committed
480
{
Don Gagne's avatar
   
Don Gagne committed
481
482
483
484
485
486
}

void LogReplayLinkController::setLink(LogReplayLink* link)
{
    if (_link) {
        disconnect(_link);
Don Gagne's avatar
   
Don Gagne committed
487
        disconnect(this, &LogReplayLinkController::playbackSpeedChanged, _link, &LogReplayLink::setPlaybackSpeed);
Don Gagne's avatar
   
Don Gagne committed
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
        _isPlaying = false;
        _percentComplete = 0;
        _playheadTime.clear();
        _totalTime.clear();
        _link = nullptr;
        emit isPlayingChanged(false);
        emit percentCompleteChanged(0);
        emit playheadTimeChanged(QString());
        emit totalTimeChanged(QString());
        emit linkChanged(nullptr);
    }


    if (link) {
        _link = link;
Don Gagne's avatar
   
Don Gagne committed
503

Don Gagne's avatar
   
Don Gagne committed
504
505
506
507
508
509
        connect(_link, &LogReplayLink::logFileStats,                      this, &LogReplayLinkController::_logFileStats);
        connect(_link, &LogReplayLink::playbackStarted,                   this, &LogReplayLinkController::_playbackStarted);
        connect(_link, &LogReplayLink::playbackPaused,                    this, &LogReplayLinkController::_playbackPaused);
        connect(_link, &LogReplayLink::playbackPercentCompleteChanged,    this, &LogReplayLinkController::_playbackPercentCompleteChanged);
        connect(_link, &LogReplayLink::currentLogTimeSecs,                this, &LogReplayLinkController::_currentLogTimeSecs);
        connect(_link, &LogReplayLink::disconnected,                      this, &LogReplayLinkController::_linkDisconnected);
Don Gagne's avatar
   
Don Gagne committed
510
511
512

        connect(this, &LogReplayLinkController::playbackSpeedChanged, _link, &LogReplayLink::setPlaybackSpeed);

Don Gagne's avatar
   
Don Gagne committed
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
        emit linkChanged(_link);
    }
}

void LogReplayLinkController::setIsPlaying(bool isPlaying)
{
    if (isPlaying) {
        _link->play();
    } else {
        _link->pause();
    }
}

void LogReplayLinkController::setPercentComplete(qreal percentComplete)
{
    _link->movePlayhead(percentComplete);
}

Don Gagne's avatar
   
Don Gagne committed
531
void LogReplayLinkController::_logFileStats(int logDurationSecs)
Don Gagne's avatar
   
Don Gagne committed
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
{
    _totalTime = _secondsToHMS(logDurationSecs);
    emit totalTimeChanged(_totalTime);
}

void LogReplayLinkController::_playbackStarted(void)
{
    _isPlaying = true;
    emit isPlayingChanged(true);
}

void LogReplayLinkController::_playbackPaused(void)
{
    _isPlaying = false;
    emit isPlayingChanged(true);
}

void LogReplayLinkController::_playbackAtEnd(void)
{
    _isPlaying = false;
    emit isPlayingChanged(true);
}

void LogReplayLinkController::_playbackPercentCompleteChanged(qreal percentComplete)
{
    _percentComplete = percentComplete;
    emit percentCompleteChanged(_percentComplete);
}

void LogReplayLinkController::_currentLogTimeSecs(int secs)
{
    if (_playheadSecs != secs) {
        _playheadSecs = secs;
        _playheadTime = _secondsToHMS(secs);
        emit playheadTimeChanged(_playheadTime);
    }
}

void LogReplayLinkController::_linkDisconnected(void)
{
    setLink(nullptr);
}

QString LogReplayLinkController::_secondsToHMS(int seconds)
{
    int secondsPart  = seconds;
    int minutesPart  = secondsPart / 60;
    int hoursPart    = minutesPart / 60;
    secondsPart -= 60 * minutesPart;
    minutesPart -= 60 * hoursPart;

Don Gagne's avatar
   
Don Gagne committed
583
584
585
586
587
    if (hoursPart == 0) {
        return tr("%2m:%3s").arg(minutesPart, 2, 10, QLatin1Char('0')).arg(secondsPart, 2, 10, QLatin1Char('0'));
    } else {
        return tr("%1h:%2m:%3s").arg(hoursPart, 2, 10, QLatin1Char('0')).arg(minutesPart, 2, 10, QLatin1Char('0')).arg(secondsPart, 2, 10, QLatin1Char('0'));
    }
Don Gagne's avatar
   
Don Gagne committed
588
}