From 2777628889f0610016d4316669d815f0d1eb1fe0 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 25 Jul 2015 10:58:44 -0700 Subject: [PATCH] Convert Log Replay to Link --- QGCApplication.pro | 5 + src/comm/LinkConfiguration.cc | 7 + src/comm/LinkConfiguration.h | 3 +- src/comm/LinkInterface.h | 3 + src/comm/LinkManager.cc | 7 + src/comm/LinkManager.h | 2 + src/comm/LogReplayLink.cc | 528 ++++++++++++++++ src/comm/LogReplayLink.h | 164 +++++ src/comm/SerialLink.cc | 15 +- src/comm/SerialLink.h | 4 +- src/uas/UAS.cc | 11 + src/uas/UAS.h | 1 + src/uas/UASInterface.h | 3 + src/ui/LogReplayLinkConfigurationWidget.cc | 53 ++ src/ui/LogReplayLinkConfigurationWidget.h | 48 ++ src/ui/LogReplayLinkConfigurationWidget.ui | 142 +++++ src/ui/QGCCommConfiguration.cc | 9 + src/ui/QGCLinkConfiguration.cc | 7 + src/ui/QGCMAVLinkLogPlayer.cc | 676 ++++----------------- src/ui/QGCMAVLinkLogPlayer.h | 104 +--- src/ui/QGCMAVLinkLogPlayer.ui | 4 +- 21 files changed, 1125 insertions(+), 671 deletions(-) create mode 100644 src/comm/LogReplayLink.cc create mode 100644 src/comm/LogReplayLink.h create mode 100644 src/ui/LogReplayLinkConfigurationWidget.cc create mode 100644 src/ui/LogReplayLinkConfigurationWidget.h create mode 100644 src/ui/LogReplayLinkConfigurationWidget.ui diff --git a/QGCApplication.pro b/QGCApplication.pro index a91292bd9..f1d3507de 100644 --- a/QGCApplication.pro +++ b/QGCApplication.pro @@ -161,6 +161,7 @@ FORMS += \ src/QGCQmlWidgetHolder.ui \ src/ui/HDDisplay.ui \ src/ui/Linechart.ui \ + src/ui/LogReplayLinkConfigurationWidget.ui \ src/ui/MainWindow.ui \ src/ui/map/QGCMapTool.ui \ src/ui/map/QGCMapToolBar.ui \ @@ -228,6 +229,7 @@ HEADERS += \ src/comm/LinkConfiguration.h \ src/comm/LinkInterface.h \ src/comm/LinkManager.h \ + src/comm/LogReplayLink.h \ src/comm/MAVLinkProtocol.h \ src/comm/MockLink.h \ src/comm/MockLinkFileServer.h \ @@ -276,6 +278,7 @@ HEADERS += \ src/ui/linechart/LinechartWidget.h \ src/ui/linechart/Scrollbar.h \ src/ui/linechart/ScrollZoomer.h \ + src/ui/LogReplayLinkConfigurationWidget.h \ src/ui/MainWindow.h \ src/ui/map/MAV2DIcon.h \ src/ui/map/QGCMapTool.h \ @@ -365,6 +368,7 @@ SOURCES += \ src/CmdLineOptParser.cc \ src/comm/LinkConfiguration.cc \ src/comm/LinkManager.cc \ + src/comm/LogReplayLink.cc \ src/comm/MAVLinkProtocol.cc \ src/comm/MockLink.cc \ src/comm/MockLinkFileServer.cc \ @@ -405,6 +409,7 @@ SOURCES += \ src/ui/linechart/LinechartWidget.cc \ src/ui/linechart/Scrollbar.cc \ src/ui/linechart/ScrollZoomer.cc \ + src/ui/LogReplayLinkConfigurationWidget.cc \ src/ui/MainWindow.cc \ src/ui/map/MAV2DIcon.cc \ src/ui/map/QGCMapTool.cc \ diff --git a/src/comm/LinkConfiguration.cc b/src/comm/LinkConfiguration.cc index 6a8d4c940..bf40204ca 100644 --- a/src/comm/LinkConfiguration.cc +++ b/src/comm/LinkConfiguration.cc @@ -33,6 +33,7 @@ This file is part of the QGROUNDCONTROL project #endif #include "UDPLink.h" #include "TCPLink.h" +#include "LogReplayLink.h" #ifdef QT_DEBUG #include "MockLink.h" @@ -95,6 +96,9 @@ LinkConfiguration* LinkConfiguration::createSettings(int type, const QString& na case LinkConfiguration::TypeTcp: config = new TCPConfiguration(name); break; + case LinkConfiguration::TypeLogReplay: + config = new LogReplayLinkConfiguration(name); + break; #ifdef QT_DEBUG case LinkConfiguration::TypeMock: config = new MockConfiguration(name); @@ -123,6 +127,9 @@ LinkConfiguration* LinkConfiguration::duplicateSettings(LinkConfiguration* sourc case TypeTcp: dupe = new TCPConfiguration(dynamic_cast(source)); break; + case TypeLogReplay: + dupe = new LogReplayLinkConfiguration(dynamic_cast(source)); + break; #ifdef QT_DEBUG case TypeMock: dupe = new MockConfiguration(dynamic_cast(source)); diff --git a/src/comm/LinkConfiguration.h b/src/comm/LinkConfiguration.h index 02fec412a..8e59bad93 100644 --- a/src/comm/LinkConfiguration.h +++ b/src/comm/LinkConfiguration.h @@ -44,13 +44,14 @@ public: #endif TypeUdp, ///< UDP Link TypeTcp, ///< TCP Link - // TODO Below is not yet implemented #if 0 + // TODO Below is not yet implemented TypeForwarding, ///< Forwarding Link TypeXbee, ///< XBee Proprietary Link TypeOpal, ///< Opal-RT Link #endif TypeMock, ///< Mock Link for Unitesting + TypeLogReplay, TypeLast // Last type value (type >= TypeLast == invalid) }; diff --git a/src/comm/LinkInterface.h b/src/comm/LinkInterface.h index f41d0f54e..68bcf9f27 100644 --- a/src/comm/LinkInterface.h +++ b/src/comm/LinkInterface.h @@ -91,6 +91,9 @@ public: * @return The nominal data rate of the interface in bit per second, 0 if unknown **/ virtual qint64 getConnectionSpeed() const = 0; + + /// @return true: This link is replaying a log file, false: Normal two-way communication link + virtual bool isLogReplay(void) { return false; } /** * @Brief Get the current incoming data rate. diff --git a/src/comm/LinkManager.cc b/src/comm/LinkManager.cc index 8c556888c..793d3f322 100644 --- a/src/comm/LinkManager.cc +++ b/src/comm/LinkManager.cc @@ -96,6 +96,9 @@ LinkInterface* LinkManager::createConnectedLink(LinkConfiguration* config) case LinkConfiguration::TypeTcp: pLink = new TCPLink(dynamic_cast(config)); break; + case LinkConfiguration::TypeLogReplay: + pLink = new LogReplayLink(dynamic_cast(config)); + break; #ifdef QT_DEBUG case LinkConfiguration::TypeMock: pLink = new MockLink(dynamic_cast(config)); @@ -394,6 +397,10 @@ void LinkManager::loadLinkConfigurationList() pLink = (LinkConfiguration*)new TCPConfiguration(name); pLink->setPreferred(preferred); break; + case LinkConfiguration::TypeLogReplay: + pLink = (LinkConfiguration*)new LogReplayLinkConfiguration(name); + pLink->setPreferred(preferred); + break; #ifdef QT_DEBUG case LinkConfiguration::TypeMock: pLink = (LinkConfiguration*)new MockConfiguration(name); diff --git a/src/comm/LinkManager.h b/src/comm/LinkManager.h index f6bde5934..24e3f8e85 100644 --- a/src/comm/LinkManager.h +++ b/src/comm/LinkManager.h @@ -41,6 +41,7 @@ This file is part of the PIXHAWK project #endif #include "UDPLink.h" #include "TCPLink.h" +#include "LogReplayLink.h" #ifdef QT_DEBUG #include "MockLink.h" @@ -110,6 +111,7 @@ public: void setConnectionsAllowed(void) { _connectionsSuspended = false; } /// Creates, connects (and adds) a link based on the given configuration instance. + /// Link takes ownership of config. LinkInterface* createConnectedLink(LinkConfiguration* config); /// Creates, connects (and adds) a link based on the given configuration name. diff --git a/src/comm/LogReplayLink.cc b/src/comm/LogReplayLink.cc new file mode 100644 index 000000000..e4bdc11d1 --- /dev/null +++ b/src/comm/LogReplayLink.cc @@ -0,0 +1,528 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2015 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "LogReplayLink.h" +#include "LinkManager.h" + +#include + +const char* LogReplayLinkConfiguration::_logFilenameKey = "logFilename"; + +const char* LogReplayLink::_errorTitle = "Log Replay Error"; + +LogReplayLinkConfiguration::LogReplayLinkConfiguration(const QString& name) : +LinkConfiguration(name) +{ + +} + +LogReplayLinkConfiguration::LogReplayLinkConfiguration(LogReplayLinkConfiguration* copy) : +LinkConfiguration(copy) +{ + _logFilename = copy->logFilename(); +} + +void LogReplayLinkConfiguration::copyFrom(LinkConfiguration *source) +{ + LinkConfiguration::copyFrom(source); + LogReplayLinkConfiguration* ssource = dynamic_cast(source); + Q_ASSERT(ssource != NULL); + _logFilename = ssource->logFilename(); +} + +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(); +} + +void LogReplayLinkConfiguration::updateSettings(void) +{ + // Doesn't support changing filename on the fly is already connected +} + +QString LogReplayLinkConfiguration::logFilenameShort(void) +{ + QFileInfo fi(_logFilename); + return fi.fileName(); +} + +LogReplayLink::LogReplayLink(LogReplayLinkConfiguration* config) : + _connected(false), + _replayAccelerationFactor(1.0f) +{ + Q_ASSERT(config); + _config = config; + + _readTickTimer.moveToThread(this); + + 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::_setAccelerationFactorOnThread, this, &LogReplayLink::_setAccelerationFactor); + + moveToThread(this); +} + +LogReplayLink::~LogReplayLink(void) +{ + _disconnect(); +} + +bool LogReplayLink::_connect(void) +{ + // Disallow replay when any links are connected + if (LinkManager::instance()->anyConnectedLinks()) { + emit communicationError(_errorTitle, "You must close all connections prior to replaying a log."); + return false; + } + + if (isRunning()) { + quit(); + wait(); + } + start(HighPriority); + return true; +} + +bool LogReplayLink::_disconnect(void) +{ + if (_connected) { + quit(); + wait(); + _connected = false; + emit disconnected(); + } + return true; +} + +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(); +} + +void LogReplayLink::_replayError(const QString& errorMsg) +{ + qDebug() << _errorTitle << errorMsg; + emit communicationError(_errorTitle, errorMsg); +} + +void LogReplayLink::readBytes(void) +{ + // FIXME: This is a bad virtual from LinkInterface? +} + +/// Since this is log replay, we just drops writes on the floor +void LogReplayLink::writeBytes(const char* bytes, qint64 cBytes) +{ + Q_UNUSED(bytes); + Q_UNUSED(cBytes); +} + +/// 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) +{ + quint64 timestamp = qFromBigEndian(*((quint64*)(bytes.constData()))); + 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; +} + +/// Seeks to the beginning of the next successully parsed mavlink message in the log file. +/// @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) +{ + char nextByte; + mavlink_status_t comm; + while (_logFile.getChar(&nextByte)) { // Loop over every byte + bool messageFound = mavlink_parse_char(getMavlinkChannel(), nextByte, nextMsg, &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() - (nextMsg->len + MAVLINK_NUM_NON_PAYLOAD_BYTES + cbTimestamp)); + QByteArray rawTime = _logFile.read(cbTimestamp); + return _parseTimestamp(rawTime); + } + } + + return 0; +} + +bool LogReplayLink::_loadLogFile(void) +{ + QString errorMsg; + QString logFilename = _config->logFilename(); + QFileInfo logFileInfo; + int logDurationSecondsTotal; + + if (_logFile.isOpen()) { + errorMsg = "Attempt to load new log while log being played"; + goto Error; + } + + _logFile.setFileName(logFilename); + if (!_logFile.open(QFile::ReadOnly)) { + errorMsg = QString("Unable to open log file: '%1', error: %2").arg(logFilename).arg(_logFile.errorString()); + goto Error; + } + logFileInfo.setFile(logFilename); + _logFileSize = logFileInfo.size(); + + _logTimestamped = logFilename.endsWith(".mavlink"); + + if (_logTimestamped) { + // Get the first timestamp from the log + // This should be a big-endian uint64. + QByteArray timestamp = _logFile.read(cbTimestamp); + quint64 startTimeUSecs = _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 - cbTimestamp; + _logFile.seek(fileLoc); + quint64 endTimeUSecs = startTimeUSecs; // Set a sane default for the endtime + mavlink_message_t msg; + quint64 messageTimeUSecs; + while ((messageTimeUSecs = _seekToNextMavlinkMessage(&msg)) > endTimeUSecs) { + endTimeUSecs = messageTimeUSecs; + } + + if (endTimeUSecs == startTimeUSecs) { + errorMsg = QString("The log file '%1' is corrupt. No valid timestamps were found at the end of the file.").arg(logFilename); + goto Error; + } + + // 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; + } 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) + { + 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; + } + } + + logDurationSecondsTotal = logFileInfo.size() / (_binaryBaudRate / 10); + } + + emit logFileStats(_logTimestamped, logDurationSecondsTotal, _binaryBaudRate); + + 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) +{ + // If we have a file with timestamps, try and pace this out following the time differences + // between the timestamps and the current playback speed. + if (_logTimestamped) { + // Now parse MAVLink messages, grabbing their timestamps as we go. We stop once we + // 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 timeToNextExecutionMSecs = 0; + + // We use the `_seekToNextMavlinkMessage()` 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; + _seekToNextMavlinkMessage(&msg); + + while (timeToNextExecutionMSecs < 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 bytesReceived(this, message); + emit playbackPercentCompleteChanged(((float)(_logCurrentTimeUSecs - _logStartTimeUSecs) / (float)_logDurationUSecs) * 100); + + // If we've reached the end of the of the file, make sure we handle that well + if (_logFile.atEnd()) { + _finishPlayback(); + return; + } + + // Run our parser to find the next timestamp and leave us at the start of the next MAVLink message. + _logCurrentTimeUSecs = _seekToNextMavlinkMessage(&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 timeDiffMSecs = ((_logCurrentTimeUSecs - _logStartTimeUSecs) / 1000) / _replayAccelerationFactor; + quint64 desiredPacedTimeMSecs = _playbackStartTimeMSecs + timeDiffMSecs; + quint64 currentTimeMSecs = (quint64)QDateTime::currentMSecsSinceEpoch(); + timeToNextExecutionMSecs = desiredPacedTimeMSecs - currentTimeMSecs; + } + + // And schedule the next execution of this function. + _readTickTimer.start(timeToNextExecutionMSecs); + } + else + { + // Binary format - read at fixed rate + const int len = 100; + QByteArray chunk = _logFile.read(len); + + emit bytesReceived(this, chunk); + emit playbackPercentCompleteChanged(((float)_logFile.pos() / (float)_logFileSize) * 100); + + // Check if reached end of file before reading next timestamp + if (chunk.length() < len || _logFile.atEnd()) + { + _finishPlayback(); + return; + } + } + +} + +void LogReplayLink::_play(void) +{ + // FIXME: With move to link I don't think this is needed any more? Except for the replay widget handling multi-uas? + LinkManager::instance()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay.")); + MAVLinkProtocol::instance()->suspendLogForReplay(true); + + // 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(); + } + + // 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() + _playbackStartTimeMSecs = (quint64)QDateTime::currentMSecsSinceEpoch() - ((_logCurrentTimeUSecs - _logStartTimeUSecs) / 1000); + + // Start timer + if (_logTimestamped) { + _readTickTimer.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); + _readTickTimer.start(interval / _replayAccelerationFactor); + } + + emit playbackStarted(); +} + +void LogReplayLink::_pause(void) +{ + LinkManager::instance()->setConnectionsAllowed(); + MAVLinkProtocol::instance()->suspendLogForReplay(false); + + _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; + _logCurrentTimeUSecs = _logStartTimeUSecs; +} + +void LogReplayLink::movePlayhead(int percentComplete) +{ + if (isPlaying()) { + qWarning() << "Should not move playhead while playing, pause first"; + return; + } + + if (percentComplete < 0 || percentComplete > 100) { + qWarning() << "Bad percentage value" << percentComplete; + return; + } + + float floatPercentComplete = (float)percentComplete / 100.0f; + + if (_logTimestamped) { + // 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)(floatPercentComplete * (float)_logFile.size()); + + // Now seek to the appropriate position, failing gracefully if we can't. + if (!_logFile.seek(newFilePos)) { + _replayError("Unable to seek to new position"); + return; + } + + // 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. + float newRelativeTimeUSecs = (float)(_logCurrentTimeUSecs - _logStartTimeUSecs); + + // Calculate the effective baud rate of the file in bytes/s. + float baudRate = _logFile.size() / (float)_logDurationUSecs / 1e6; + + // And the desired time is: + float desiredTimeUSecs = floatPercentComplete * _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("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); + + // Now update the UI with our actual final position. + newRelativeTimeUSecs = (float)(_logCurrentTimeUSecs - _logStartTimeUSecs); + percentComplete = (newRelativeTimeUSecs / _logDurationUSecs) * 100; + emit playbackPercentCompleteChanged(percentComplete); + } else { + // 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)(floatPercentComplete * (float)_logFile.size()); + + // Now seek to the appropriate position, failing gracefully if we can't. + if (!_logFile.seek(newFilePos)) { + _replayError("Unable to seek to new position"); + return; + } + + // But we do align to the next MAVLink message for consistency. + mavlink_message_t dummy; + _seekToNextMavlinkMessage(&dummy); + } +} + +void LogReplayLink::_setAccelerationFactor(int factor) +{ + // factor: -100: 0.01X, 0: 1.0X, 100: 100.0X + + if (factor < 0) { + _replayAccelerationFactor = 0.01f; + factor -= -100; + if (factor > 0) { + _replayAccelerationFactor *= (float)factor; + } + } else if (factor > 0) { + _replayAccelerationFactor = 1.0f * (float)factor; + } else { + _replayAccelerationFactor = 1.0f; + } + + // Update timer interval + if (!_logTimestamped) { + // 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); + _readTickTimer.stop(); + _readTickTimer.start(interval / _replayAccelerationFactor); + } +} + +/// @brief Called when playback is complete +void LogReplayLink::_finishPlayback(void) +{ + _pause(); + + emit playbackAtEnd(); +} + +/// @brief Called when an error occurs during playback to reset playback system state. +void LogReplayLink::_playbackError(void) +{ + _pause(); + _logFile.close(); + emit playbackError(); +} diff --git a/src/comm/LogReplayLink.h b/src/comm/LogReplayLink.h new file mode 100644 index 000000000..556c95c53 --- /dev/null +++ b/src/comm/LogReplayLink.h @@ -0,0 +1,164 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2015 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#ifndef LogReplayLink_H +#define LogReplayLink_H + +#include "LinkInterface.h" +#include "LinkConfiguration.h" +#include "MAVLinkProtocol.h" + +#include +#include + +class LogReplayLinkConfiguration : public LinkConfiguration +{ +public: + LogReplayLinkConfiguration(const QString& name); + LogReplayLinkConfiguration(LogReplayLinkConfiguration* copy); + + QString logFilename(void) { return _logFilename; } + void setLogFilename(const QString& logFilename) { _logFilename = logFilename; } + + QString logFilenameShort(void); + + // Virtuals from LinkConfiguration + virtual int type() { return LinkConfiguration::TypeLogReplay; } + virtual void copyFrom(LinkConfiguration* source); + virtual void loadSettings(QSettings& settings, const QString& root); + virtual void saveSettings(QSettings& settings, const QString& root); + virtual void updateSettings(); + +private: + static const char* _logFilenameKey; + QString _logFilename; +}; + +class LogReplayLink : public LinkInterface +{ + Q_OBJECT + + friend class LinkManager; + +public: + /// @return true: log is currently playing, false: log playback is paused + bool isPlaying(void) { return _readTickTimer.isActive(); } + + /// Start replay at current position + void play(void) { emit _playOnThread(); } + + /// Pause replay + void pause(void) { emit _pauseOnThread(); } + + /// Move the playhead to the specified percent complete + void movePlayhead(int percentComplete); + + /// Sets the acceleration factor: -100: 0.01X, 0: 1.0X, 100: 100.0X + void setAccelerationFactor(int factor) { emit _setAccelerationFactorOnThread(factor); } + + // Virtuals from LinkInterface + virtual QString getName(void) const { return _config->name(); } + virtual void requestReset(void){ } + virtual bool isConnected(void) const { return _connected; } + virtual qint64 getConnectionSpeed(void) const { return 100000000; } + virtual qint64 bytesAvailable(void) { return 0; } + virtual bool isLogReplay(void) { return true; } + + // These are left unimplemented in order to cause linker errors which indicate incorrect usage of + // connect/disconnect on link directly. All connect/disconnect calls should be made through LinkManager. + bool connect(void); + bool disconnect(void); + +public slots: + virtual void writeBytes(const char *bytes, qint64 cBytes); + +signals: + void logFileStats(bool logTimestamped, int logDurationSecs, int binaryBaudRate); + void playbackStarted(void); + void playbackPaused(void); + void playbackAtEnd(void); + void playbackError(void); + void playbackPercentCompleteChanged(int percentComplete); + + // Internal signals + void _playOnThread(void); + void _pauseOnThread(void); + void _setAccelerationFactorOnThread(int factor); + +protected slots: + // FIXME: This should not be part of LinkInterface. It is an internal link implementation detail. + virtual void readBytes(void); + +private slots: + void _readNextLogEntry(void); + void _play(void); + void _pause(void); + void _setAccelerationFactor(int factor); + +private: + // Links are only created/destroyed by LinkManager so constructor/destructor is not public + LogReplayLink(LogReplayLinkConfiguration* config); + ~LogReplayLink(); + + void _replayError(const QString& errorMsg); + quint64 _parseTimestamp(const QByteArray& bytes); + quint64 _seekToNextMavlinkMessage(mavlink_message_t* nextMsg); + bool _loadLogFile(void); + void _finishPlayback(void); + void _playbackError(void); + void _resetPlaybackToBeginning(void); + + // Virtuals from LinkInterface + virtual bool _connect(void); + virtual bool _disconnect(void); + + // Virtuals from QThread + virtual void run(void); + + LogReplayLinkConfiguration* _config; + + bool _connected; + QTimer _readTickTimer; ///< Timer which signals a read of next log record + + static const char* _errorTitle; ///< Title for communicatorError signals + + quint64 _logCurrentTimeUSecs; ///< The timestamp of the next message in the log file. + quint64 _logStartTimeUSecs; ///< The first timestamp in the current log file. + quint64 _logEndTimeUSecs; ///< The last timestamp in the current log file. + quint64 _logDurationUSecs; + + static const int _defaultBinaryBaudRate = 57600; + int _binaryBaudRate; ///< Playback rate for binary log format + + float _replayAccelerationFactor; ///< Factor to apply to playback rate + quint64 _playbackStartTimeMSecs; ///< The time when the logfile was first played back. This is used to pace out replaying the messages to fix long-term drift/skew. 0 indicates that the player hasn't initiated playback of this log file. + + MAVLinkProtocol* _mavlink; + QFile _logFile; + quint64 _logFileSize; + bool _logTimestamped; ///< true: Timestamped log format, false: no timestamps + + static const int cbTimestamp = sizeof(quint64); +}; + +#endif diff --git a/src/comm/SerialLink.cc b/src/comm/SerialLink.cc index adcdd529a..68c0f9700 100644 --- a/src/comm/SerialLink.cc +++ b/src/comm/SerialLink.cc @@ -353,17 +353,12 @@ qint64 SerialLink::getConnectionSpeed() const void SerialLink::_resetConfiguration() { - bool somethingChanged = false; if (_port) { - somethingChanged = _port->setBaudRate (_config->baud()); - somethingChanged |= _port->setDataBits (static_cast (_config->dataBits())); - somethingChanged |= _port->setFlowControl (static_cast (_config->flowControl())); - somethingChanged |= _port->setStopBits (static_cast (_config->stopBits())); - somethingChanged |= _port->setParity (static_cast (_config->parity())); - } - if(somethingChanged) { - qCDebug(SerialLinkLog) << "Reconfiguring port"; - emit updateLink(this); + _port->setBaudRate (_config->baud()); + _port->setDataBits (static_cast (_config->dataBits())); + _port->setFlowControl (static_cast (_config->flowControl())); + _port->setStopBits (static_cast (_config->stopBits())); + _port->setParity (static_cast (_config->parity())); } } diff --git a/src/comm/SerialLink.h b/src/comm/SerialLink.h index 55b262044..fbbeeea39 100644 --- a/src/comm/SerialLink.h +++ b/src/comm/SerialLink.h @@ -119,14 +119,12 @@ public: void requestReset(); bool isConnected() const; qint64 getConnectionSpeed() const; + // These are left unimplemented in order to cause linker errors which indicate incorrect usage of // connect/disconnect on link directly. All connect/disconnect calls should be made through LinkManager. bool connect(void); bool disconnect(void); -signals: //[TODO] Refactor to Linkinterface - void updateLink(LinkInterface*); - public slots: void readBytes(); diff --git a/src/uas/UAS.cc b/src/uas/UAS.cc index db6bec2d6..1cd89b695 100644 --- a/src/uas/UAS.cc +++ b/src/uas/UAS.cc @@ -3414,3 +3414,14 @@ bool UAS::_containsLink(LinkInterface* link) return false; } + +bool UAS::isLogReplay(void) +{ + QList links = getLinks(); + + if (links.count() == 1) { + return links[0]->isLogReplay(); + } else { + return false; + } +} diff --git a/src/uas/UAS.h b/src/uas/UAS.h index caf630412..a9519ec0d 100644 --- a/src/uas/UAS.h +++ b/src/uas/UAS.h @@ -93,6 +93,7 @@ public: float filterVoltage(float value) const; /** @brief Get the links associated with this robot */ QList getLinks(); + bool isLogReplay(void); Q_PROPERTY(double localX READ getLocalX WRITE setLocalX NOTIFY localXChanged) Q_PROPERTY(double localY READ getLocalY WRITE setLocalY NOTIFY localYChanged) diff --git a/src/uas/UASInterface.h b/src/uas/UASInterface.h index f535abba8..9c3bbf1a9 100644 --- a/src/uas/UASInterface.h +++ b/src/uas/UASInterface.h @@ -166,6 +166,9 @@ public: * interface. The LinkInterface can support multiple protocols. **/ virtual QList getLinks() = 0; + + /// @returns true: UAS is connected to log replay link + virtual bool isLogReplay(void) = 0; /** * @brief Get the color for this UAS diff --git a/src/ui/LogReplayLinkConfigurationWidget.cc b/src/ui/LogReplayLinkConfigurationWidget.cc new file mode 100644 index 000000000..8bff2dbf9 --- /dev/null +++ b/src/ui/LogReplayLinkConfigurationWidget.cc @@ -0,0 +1,53 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009, 2015 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "LogReplayLinkConfigurationWidget.h" +#include "QGCFileDialog.h" +#include "QGCApplication.h" + +LogReplayLinkConfigurationWidget::LogReplayLinkConfigurationWidget(LogReplayLinkConfiguration *config, QWidget *parent, Qt::WindowFlags flags) : + QWidget(parent, flags) +{ + _ui.setupUi(this); + + Q_ASSERT(config != NULL); + _config = config; + + _ui.logFilename->setText(_config->logFilename()); + + connect(_ui.selectLogFileButton, &QPushButton::clicked, this, &LogReplayLinkConfigurationWidget::_selectLogFile); +} + +void LogReplayLinkConfigurationWidget::_selectLogFile(bool checked) +{ + Q_UNUSED(checked); + + QString logFile = QGCFileDialog::getOpenFileName(this, + "Select log file to replay", + qgcApp()->mavlinkLogFilesLocation(), + "MAVLink Log Files (*.mavlink);;All Files (*)"); + if (!logFile.isEmpty()) { + _ui.logFilename->setText(logFile); + _config->setLogFilename(logFile); + } +} \ No newline at end of file diff --git a/src/ui/LogReplayLinkConfigurationWidget.h b/src/ui/LogReplayLinkConfigurationWidget.h new file mode 100644 index 000000000..6d36abb07 --- /dev/null +++ b/src/ui/LogReplayLinkConfigurationWidget.h @@ -0,0 +1,48 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009, 2015 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#ifndef _LogReplayLinkConfigurationWidget_H_ +#define _LogReplayLinkConfigurationWidget_H_ + +#include + +#include "LogReplayLink.h" +#include "ui_LogReplayLinkConfigurationWidget.h" + +class LogReplayLinkConfigurationWidget : public QWidget +{ + Q_OBJECT + +public: + LogReplayLinkConfigurationWidget(LogReplayLinkConfiguration* config, QWidget *parent = 0, Qt::WindowFlags flags = Qt::Sheet); + +private slots: + void _selectLogFile(bool checked); + +private: + Ui::LogReplayLinkConfigurationWidget _ui; + LogReplayLinkConfiguration* _config; +}; + + +#endif diff --git a/src/ui/LogReplayLinkConfigurationWidget.ui b/src/ui/LogReplayLinkConfigurationWidget.ui new file mode 100644 index 000000000..cd876d5d6 --- /dev/null +++ b/src/ui/LogReplayLinkConfigurationWidget.ui @@ -0,0 +1,142 @@ + + + LogReplayLinkConfigurationWidget + + + + 0 + 0 + 325 + 347 + + + + Form + + + + + + + + + 0 + 0 + + + + Log File: + + + + + + + + + + 0 + 0 + + + + + 0 + 60 + + + + + + + true + + + + + + + Select Log File + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Delete + + + Delete this link + + + Delete this link + + + Link delete button + + + + + Connect + + + Connect this link + + + Connect this link + + + Connect this link + + + + + Close + + + Close the configuration window + + + Close the configuration window + + + Close the configuration window + + + + + + + actionClose + triggered() + LogReplayLinkConfigurationWidget + close() + + + -1 + -1 + + + 224 + 195 + + + + + diff --git a/src/ui/QGCCommConfiguration.cc b/src/ui/QGCCommConfiguration.cc index 6aa3ec0d1..bc32198e4 100644 --- a/src/ui/QGCCommConfiguration.cc +++ b/src/ui/QGCCommConfiguration.cc @@ -36,6 +36,7 @@ This file is part of the QGROUNDCONTROL project #endif #include "QGCUDPLinkConfiguration.h" #include "QGCTCPLinkConfiguration.h" +#include "LogReplayLinkConfigurationWidget.h" #include "QGCCommConfiguration.h" #include "ui_QGCCommConfiguration.h" @@ -52,6 +53,7 @@ QGCCommConfiguration::QGCCommConfiguration(QWidget *parent, LinkConfiguration *c #endif _ui->typeCombo->addItem(tr("UDP"), LinkConfiguration::TypeUdp); _ui->typeCombo->addItem(tr("TCP"), LinkConfiguration::TypeTcp); + _ui->typeCombo->addItem(tr("Log replay"), LinkConfiguration::TypeLogReplay); #ifdef QT_DEBUG _ui->typeCombo->addItem(tr("Mock"), LinkConfiguration::TypeMock); #endif @@ -140,6 +142,13 @@ void QGCCommConfiguration::_loadTypeConfigWidget(int type) _ui->typeCombo->setCurrentIndex(_ui->typeCombo->findData(LinkConfiguration::TypeTcp)); } break; + case LinkConfiguration::TypeLogReplay: { + QWidget* conf = new LogReplayLinkConfigurationWidget((LogReplayLinkConfiguration*)_config, this); + _ui->linkScrollArea->setWidget(conf); + _ui->linkGroupBox->setTitle("Log Replay"); + _ui->typeCombo->setCurrentIndex(_ui->typeCombo->findData(LinkConfiguration::TypeLogReplay)); + } + break; #ifdef QT_DEBUG case LinkConfiguration::TypeMock: { _ui->linkScrollArea->setWidget(NULL); diff --git a/src/ui/QGCLinkConfiguration.cc b/src/ui/QGCLinkConfiguration.cc index d0e312b8c..57c77d575 100644 --- a/src/ui/QGCLinkConfiguration.cc +++ b/src/ui/QGCLinkConfiguration.cc @@ -160,6 +160,13 @@ void QGCLinkConfiguration::_fixUnnamed(LinkConfiguration* config) } } break; + case LinkConfiguration::TypeLogReplay: { + LogReplayLinkConfiguration* tconfig = dynamic_cast(config); + if(tconfig) { + config->setName(QString("Log Replay %1").arg(tconfig->logFilenameShort())); + } + } + break; #ifdef QT_DEBUG case LinkConfiguration::TypeMock: config->setName( diff --git a/src/ui/QGCMAVLinkLogPlayer.cc b/src/ui/QGCMAVLinkLogPlayer.cc index c7d8f7c6f..9b942f556 100644 --- a/src/ui/QGCMAVLinkLogPlayer.cc +++ b/src/ui/QGCMAVLinkLogPlayer.cc @@ -15,236 +15,50 @@ 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), - isPlaying(false), - currPacketCount(0), - ui(new Ui::QGCMAVLinkLogPlayer) + _replayLink(NULL), + _ui(new Ui::QGCMAVLinkLogPlayer) { Q_ASSERT(mavlink); - ui->setupUi(this); - ui->horizontalLayout->setAlignment(Qt::AlignTop); - - // Connect protocol - connect(this, SIGNAL(bytesReady(LinkInterface*,QByteArray)), mavlink, SLOT(receiveBytes(LinkInterface*,QByteArray))); - - // Setup timer - connect(&loopTimer, &QTimer::timeout, this, &QGCMAVLinkLogPlayer::logLoop); + _ui->setupUi(this); + _ui->horizontalLayout->setAlignment(Qt::AlignTop); // Setup buttons - 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); + connect(_ui->selectFileButton, &QPushButton::clicked, this, &QGCMAVLinkLogPlayer::_selectLogFileForPlayback); + connect(_ui->playButton, &QPushButton::clicked, this, &QGCMAVLinkLogPlayer::_playPauseToggle); + connect(_ui->speedSlider, &QSlider::valueChanged, this, &QGCMAVLinkLogPlayer::_setAccelerationFromSlider); + connect(_ui->positionSlider, &QSlider::valueChanged, this, &QGCMAVLinkLogPlayer::_setPlayheadFromSlider); + connect(_ui->positionSlider, &QSlider::sliderPressed, this, &QGCMAVLinkLogPlayer::_pause); - setAccelerationFactorInt(49); - ui->speedSlider->setValue(49); - updatePositionSliderUi(0.0); - - 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. - connect(this, &QGCMAVLinkLogPlayer::logFileEndReached, &loopTimer, &QTimer::stop); -} - -QGCMAVLinkLogPlayer::~QGCMAVLinkLogPlayer() -{ - delete ui; -} - -void QGCMAVLinkLogPlayer::playPauseToggle() -{ - if (isPlaying) - { - pause(); - } - else - { - play(); - } -} - -void QGCMAVLinkLogPlayer::play() -{ - Q_ASSERT(logFile.isOpen()); + _enablePlaybackControls(false); - LinkManager::instance()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay.")); - mavlink->suspendLogForReplay(true); + _ui->positionSlider->setMinimum(0); + _ui->positionSlider->setMaximum(100); - // 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()) - { - reset(); - } - - // Always correct the current start time such that the next message will play immediately at playback. - // We do this by subtracting the current file playback offset from now() - playbackStartTime = (quint64)QDateTime::currentMSecsSinceEpoch() - (logCurrentTime - logStartTime) / 1000; - - // Start timer - 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->setChecked(true); - ui->playButton->setIcon(QIcon(":/res/Pause")); -} - -void QGCMAVLinkLogPlayer::pause() -{ - LinkManager::instance()->setConnectionsAllowed(); - mavlink->suspendLogForReplay(false); - - loopTimer.stop(); - isPlaying = false; - ui->playButton->setIcon(QIcon(":/res/Play")); - ui->playButton->setChecked(false); - ui->selectFileButton->setEnabled(true); + _ui->speedSlider->setMinimum(-100); + _ui->speedSlider->setMaximum(100); + _ui->speedSlider->setValue(0); } -void QGCMAVLinkLogPlayer::reset() +QGCMAVLinkLogPlayer::~QGCMAVLinkLogPlayer() { - pause(); - loopCounter = 0; - 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; + delete _ui; } -bool QGCMAVLinkLogPlayer::jumpToPlaybackLocation(float percentage) +void QGCMAVLinkLogPlayer::_playPauseToggle(void) { - // Reset only for valid values - if (percentage <= 100.0f && percentage >= 0.0f) - { - 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); - - // 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 - { - // 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; - } - else - { - return false; + if (_replayLink->isPlaying()) { + _pause(); + } else { + _replayLink->play(); } } -void QGCMAVLinkLogPlayer::updatePositionSliderUi(float percent) +void QGCMAVLinkLogPlayer::_pause(void) { - 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); + _replayLink->pause(); } -/// @brief Displays a file dialog to allow the user to select a log file to play back. void QGCMAVLinkLogPlayer::_selectLogFileForPlayback(void) { // Disallow replay when any links are connected @@ -253,403 +67,127 @@ void QGCMAVLinkLogPlayer::_selectLogFileForPlayback(void) return; } - QString logFile = QGCFileDialog::getOpenFileName( + QString logFilename = QGCFileDialog::getOpenFileName( this, tr("Load MAVLink Log File"), qgcApp()->mavlinkLogFilesLocation(), tr("MAVLink Log Files (*.mavlink);;All Files (*)")); - if (!logFile.isEmpty()) { - loadLogFile(logFile); + if (logFilename.isEmpty()) { + return; } + + LinkInterface* createConnectedLink(LinkConfiguration* config); + + LogReplayLinkConfiguration* linkConfig = new LogReplayLinkConfiguration(QString("Log Replay")); + linkConfig->setLogFilename(logFilename); + linkConfig->setName(linkConfig->logFilenameShort()); + _ui->logFileNameLabel->setText(linkConfig->logFilenameShort()); + _replayLink = (LogReplayLink*)LinkManager::instance()->createConnectedLink(linkConfig); + + connect(_replayLink, &LogReplayLink::logFileStats, this, &QGCMAVLinkLogPlayer::_logFileStats); + connect(_replayLink, &LogReplayLink::playbackStarted, this, &QGCMAVLinkLogPlayer::_playbackStarted); + connect(_replayLink, &LogReplayLink::playbackPaused, this, &QGCMAVLinkLogPlayer::_playbackPaused); + connect(_replayLink, &LogReplayLink::playbackPercentCompleteChanged, this, &QGCMAVLinkLogPlayer::_playbackPercentCompleteChanged); + connect(_replayLink, &LogReplayLink::disconnected, this, &QGCMAVLinkLogPlayer::_replayLinkDisconnected); + + _ui->positionSlider->setValue(0); + _ui->speedSlider->setValue(0); } -/** - * @param factor 0: 0.01X, 50: 1.0X, 100: 100.0X - */ -void QGCMAVLinkLogPlayer::setAccelerationFactorInt(int factor) +void QGCMAVLinkLogPlayer::_playbackError(void) { - 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); - } - - ui->speedLabel->setText(tr("Speed: %1X").arg(accelerationFactor, 5, 'f', 2, '0')); + _ui->logFileNameLabel->setText("Error"); + _enablePlaybackControls(false); } -bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file) +QString QGCMAVLinkLogPlayer::_secondsToHMS(int seconds) { - // Make sure to stop the logging process and reset everything. - reset(); - logFile.close(); - - // Now load the new file. - logFile.setFileName(file); - if (!logFile.open(QFile::ReadOnly)) { - 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)); - _playbackError(); - return false; - } - - QFileInfo logFileInfo(file); - ui->logFileNameLabel->setText(tr("File: %1").arg(logFileInfo.fileName())); - - // If there's an existing MAVLinkSimulationLink() being used for an old file, - // we replace it. - if (logLink) { - LinkManager::instance()->_deleteLink(logLink); - } - logLink = new MockLink(); - LinkManager::instance()->_addLink(logLink); + int secondsPart = seconds; + int minutesPart = secondsPart / 60; + int hoursPart = minutesPart / 60; + secondsPart -= 60 * minutesPart; + minutesPart -= 60 * hoursPart; - // Select if binary or MAVLink log format is used - mavlinkLogFormat = file.endsWith(".mavlink"); - - 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; - } - - if (endtime == starttime) { - QGCMessageBox::critical(tr("Log Replay"), tr("The selected file is corrupt. No valid timestamps were found at the end of the file.").arg(file)); - _playbackError(); - return false; - } - - // Remember the start and end time so we can move around this logfile with the slider. - logEndTime = endtime; - logStartTime = starttime; - logCurrentTime = logStartTime; - - // Reset our log file so when we go to read it for the first time, we start at the beginning. - logFile.reset(); - - // 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) - { - 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)); - } - - // 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(); - - return true; + return QString("%1h:%2m:%3s").arg(hoursPart, 2).arg(minutesPart, 2).arg(secondsPart, 2); } -quint64 QGCMAVLinkLogPlayer::parseTimestamp(const QByteArray &data) +/// Signalled from LogReplayLink once log file information is known +void QGCMAVLinkLogPlayer::_logFileStats(bool logTimestamped, ///< true: timestamped log + int logDurationSeconds, ///< Log duration + int binaryBaudRate) ///< Baud rate for non-timestamped log { - // 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; + Q_UNUSED(logTimestamped); + Q_UNUSED(binaryBaudRate); + + _logDurationSeconds = logDurationSeconds; + + _ui->logStatsLabel->setText(_secondsToHMS(logDurationSeconds)); } -/** - * 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) +/// Signalled from LogReplayLink when replay starts +void QGCMAVLinkLogPlayer::_playbackStarted(void) { - // 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)) - { - 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)); - } - - play(); - } - else - { - reset(); - } + _enablePlaybackControls(true); + _ui->playButton->setChecked(true); + _ui->playButton->setIcon(QIcon(":/res/Pause")); } -/** - * 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() +/// Signalled from LogReplayLink when replay is paused +void QGCMAVLinkLogPlayer::_playbackPaused(void) { - // If we have a file with timestamps, try and pace this out following the time differences - // between the timestamps and the current playback speed. - if (mavlinkLogFormat) - { - // Now parse MAVLink messages, grabbing their timestamps as we go. We stop once we - // 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; - - // 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; - 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 - if (logFile.atEnd()) { - _finishPlayback(); - return; - } - - // 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); - } - 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()) - { - _finishPlayback(); - return; - } - } - - // 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) - { - QFileInfo logFileInfo(logFile); - updatePositionSliderUi(logFile.pos() / static_cast(logFileInfo.size())); - } - loopCounter++; + _ui->playButton->setIcon(QIcon(":/res/Play")); + _ui->playButton->setChecked(false); } -/** - * 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) +void QGCMAVLinkLogPlayer::_playbackPercentCompleteChanged(int percentComplete) { - char nextByte; - mavlink_status_t comm; - while (logFile.getChar(&nextByte)) { // Loop over every byte - bool messageFound = mavlink_parse_char(logLink->getMavlinkChannel(), 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; + _ui->positionSlider->blockSignals(true); + _ui->positionSlider->setValue(percentComplete); + _ui->positionSlider->blockSignals(false); } -void QGCMAVLinkLogPlayer::changeEvent(QEvent *e) +void QGCMAVLinkLogPlayer::_setPlayheadFromSlider(int value) { - QWidget::changeEvent(e); - switch (e->type()) - { - case QEvent::LanguageChange: - ui->retranslateUi(this); - break; - default: - break; + if (_replayLink) { + _replayLink->movePlayhead(value); } } -/** - * Implement paintEvent() so that stylesheets work for our custom widget. - */ -void QGCMAVLinkLogPlayer::paintEvent(QPaintEvent *) +void QGCMAVLinkLogPlayer::_enablePlaybackControls(bool enabled) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + _ui->playButton->setEnabled(enabled); + _ui->speedSlider->setEnabled(enabled); + _ui->positionSlider->setEnabled(enabled); } -/// @brief Called when playback is complete -void QGCMAVLinkLogPlayer::_finishPlayback(void) +void QGCMAVLinkLogPlayer::_setAccelerationFromSlider(int value) { - pause(); - - QString status = tr("Flight Data replay complete"); - ui->logStatsLabel->setText(status); + qDebug() << value; + if (_replayLink) { + _replayLink->setAccelerationFactor(value); + } - // 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); + // Factor: -100: 0.01x, 0: 1.0x, 100: 100x - emit logFileEndReached(); + float accelerationFactor; + if (value < 0) { + accelerationFactor = 0.01f; + value -= -100; + if (value > 0) { + accelerationFactor *= (float)value; + } + } else if (value > 0) { + accelerationFactor = 1.0f * (float)value; + } else { + accelerationFactor = 1.0f; + } - mavlink->suspendLogForReplay(false); - LinkManager::instance()->setConnectionsAllowed(); + _ui->speedLabel->setText(QString("Speed: %1X").arg(accelerationFactor, 5, 'f', 2, '0')); } -/// @brief Called when an error occurs during playback to reset playback system state. -void QGCMAVLinkLogPlayer::_playbackError(void) +void QGCMAVLinkLogPlayer::_replayLinkDisconnected(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); -} + _enablePlaybackControls(false); + _replayLink = NULL; +} \ No newline at end of file diff --git a/src/ui/QGCMAVLinkLogPlayer.h b/src/ui/QGCMAVLinkLogPlayer.h index 8e49b14d8..181355b5c 100644 --- a/src/ui/QGCMAVLinkLogPlayer.h +++ b/src/ui/QGCMAVLinkLogPlayer.h @@ -6,7 +6,7 @@ #include "MAVLinkProtocol.h" #include "LinkInterface.h" -#include "MockLink.h" +#include "LogReplayLink.h" namespace Ui { @@ -27,97 +27,29 @@ class QGCMAVLinkLogPlayer : public QWidget public: explicit QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *parent = 0); ~QGCMAVLinkLogPlayer(); - bool isPlayingLogFile() - { - return isPlaying; - } - bool isLogFileSelected() - { - return logFile.isOpen(); - } - -public slots: - /** @brief Toggle between play and pause */ - void playPauseToggle(); - /** @brief Replay the logfile */ - void play(); - /** @brief Pause the log player. */ - void pause(); - /** @brief Reset the internal log player state, including the UI */ - void reset(); - /** @brief Load log file */ - bool loadLogFile(const QString& file); - /** @brief Jump to a position in the logfile */ - void jumpToSliderVal(int slidervalue); - /** @brief The logging mainloop */ - void logLoop(); - /** @brief Set acceleration factor in percent */ - void setAccelerationFactorInt(int factor); - -signals: - /** @brief Send ready bytes */ - void bytesReady(LinkInterface* link, const QByteArray& bytes); - void logFileEndReached(); - -protected: - quint64 playbackStartTime; ///< The time when the logfile was first played back. This is used to pace out replaying the messages to fix long-term drift/skew. 0 indicates that the player hasn't initiated playback of this log file. In units of milliseconds since epoch UTC. - quint64 logCurrentTime; ///< The timestamp of the next message in the log file. In units of microseconds since epoch UTC. - quint64 logStartTime; ///< The first timestamp in the current log file. In units of microseconds since epoch UTC. - quint64 logEndTime; ///< The last timestamp in the current log file. In units of microseconds since epoch UTC. - float accelerationFactor; - MAVLinkProtocol* mavlink; - MockLink* logLink; - QFile logFile; - QTimer loopTimer; - int loopCounter; - bool mavlinkLogFormat; ///< If the logfile is stored in the timestamped MAVLink log format - int binaryBaudRate; - static const int defaultBinaryBaudRate = 57600; - bool isPlaying; - unsigned int currPacketCount; - static const int packetLen = MAVLINK_MAX_PACKET_LEN; - static const int timeLen = sizeof(quint64); - void changeEvent(QEvent *e); - private slots: void _selectLogFileForPlayback(void); + void _playPauseToggle(void); + void _pause(void); + void _setPlayheadFromSlider(int value); + void _setAccelerationFromSlider(int value); + void _logFileStats(bool logTimestamped, int logDurationSeconds, int binaryBaudRate); + void _playbackStarted(void); + void _playbackPaused(void); + void _playbackPercentCompleteChanged(int percentComplete); + void _playbackError(void); + void _replayLinkDisconnected(void); private: - Ui::QGCMAVLinkLogPlayer *ui; - virtual void paintEvent(QPaintEvent *); - - /** @brief Parse out a quint64 timestamp in microseconds in the proper endianness. */ - quint64 parseTimestamp(const QByteArray &data); - - /** - * This function parses out the next MAVLink message and its corresponding timestamp. - * - * It makes no assumptions about where in the file we currently are. It leaves the file right - * at the beginning of the successfully parsed message. Note that this function will not attempt to - * correct for any MAVLink parsing failures, so it always returns the next successfully-parsed - * message. - * - * @param msg[output] Where the final parsed message output will go. - * @return A Unix timestamp in microseconds UTC or 0 if parsing failed - */ - quint64 findNextMavlinkMessage(mavlink_message_t *msg); - - /** - * Updates the QSlider UI to be at the given percentage. - * @param percent A percentage value between 0.0% and 100.0%. - */ - void updatePositionSliderUi(float percent); + void _finishPlayback(void); + QString _secondsToHMS(int seconds); + void _enablePlaybackControls(bool enabled); - /** - * Jumps to a new position in the current playback file as a percentage. - * @param percentage The position of the file to jump to as a percentage. - * @return True if the new file position was successfully jumped to, false otherwise - */ - bool jumpToPlaybackLocation(float percentage); + LogReplayLink* _replayLink; + int _logDurationSeconds; - void _finishPlayback(void); - void _playbackError(void); + Ui::QGCMAVLinkLogPlayer* _ui; }; -#endif // QGCMAVLINKLOGPLAYER_H +#endif diff --git a/src/ui/QGCMAVLinkLogPlayer.ui b/src/ui/QGCMAVLinkLogPlayer.ui index df91cde63..663c2b852 100644 --- a/src/ui/QGCMAVLinkLogPlayer.ui +++ b/src/ui/QGCMAVLinkLogPlayer.ui @@ -6,7 +6,7 @@ 0 0 - 789 + 948 38 @@ -49,7 +49,7 @@ - :/res/Play:/res.Play + :/res/Play:/res/Play true -- 2.22.0