8#include <QtCore/QFileInfo>
9#include <QtCore/QtEndian>
10#include <QtCore/QThread>
11#include <QtCore/QTimer>
20 qCDebug(LogReplayLinkLog) <<
this;
25 , _logFilename(copy->logFilename())
27 qCDebug(LogReplayLinkLog) <<
this;
32 qCDebug(LogReplayLinkLog) <<
this;
46 settings.beginGroup(root);
55 settings.beginGroup(root);
57 settings.setValue(
"logFilename", _logFilename);
64 return QFileInfo(_logFilename).fileName();
81 qCDebug(LogReplayLinkLog) <<
this;
88 qCDebug(LogReplayLinkLog) <<
this;
93 if (!_readTickTimer) {
94 _readTickTimer =
new QTimer(
this);
97 (void) connect(_readTickTimer, &QTimer::timeout,
this, &LogReplayWorker::_readNextLogEntry);
103 qCWarning(LogReplayLinkLog) <<
"Already connected";
108 emit
errorOccurred(tr(
"You must close all connections prior to replaying a log."));
112 if (!_loadLogFile()) {
126 qCDebug(LogReplayLinkLog) <<
"Already disconnected";
130 qCDebug(LogReplayLinkLog) <<
"Disconnecting from log";
132 if (_readTickTimer) {
133 _readTickTimer->stop();
136 if (_logFile.isOpen()) {
140 _isConnected =
false;
146 return (_readTickTimer && _readTickTimer->isActive());
154 if (_logFile.atEnd()) {
155 _resetPlaybackToBeginning();
158 _playbackStartTimeMSecs =
static_cast<quint64
>(QDateTime::currentMSecsSinceEpoch());
159 _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
160 _readTickTimer->start(1);
170 _readTickTimer->stop();
177 _playbackSpeed = playbackSpeed;
178 _playbackStartTimeMSecs =
static_cast<quint64
>(QDateTime::currentMSecsSinceEpoch());
179 _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
180 _readTickTimer->start(1);
187 if (_readTickTimer->isActive()) {
192 percentComplete = qBound(0., percentComplete, 100.);
193 const qreal percentCompleteMult = percentComplete / 100.0;
194 const qint64 newFilePos =
static_cast<qint64
>(percentCompleteMult *
static_cast<qreal
>(_logFile.size()));
195 if (!_logFile.seek(newFilePos)) {
201 _logCurrentTimeUSecs = _seekToNextMavlinkMessage(dummy);
203 qreal newRelativeTimeUSecs =
static_cast<qreal
>(_logCurrentTimeUSecs - _logStartTimeUSecs);
204 const qreal baudRate = _logFile.size() /
static_cast<qreal
>(_logDurationUSecs) / 1e6;
205 const qreal desiredTimeUSecs = percentCompleteMult * _logDurationUSecs;
206 const qint64 offset = (newRelativeTimeUSecs - desiredTimeUSecs) * baudRate;
207 if (!_logFile.seek(_logFile.pos() + offset)) {
212 _logCurrentTimeUSecs = _seekToNextMavlinkMessage(dummy);
213 _signalCurrentLogTimeSecs();
215 newRelativeTimeUSecs =
static_cast<qreal
>(_logCurrentTimeUSecs - _logStartTimeUSecs);
216 percentComplete = ((newRelativeTimeUSecs / _logDurationUSecs) * 100);
220void LogReplayWorker::_resetPlaybackToBeginning()
222 if (_logFile.isOpen()) {
223 if (!_logFile.reset()) {
224 qCWarning(LogReplayLinkLog) <<
"failed to reset log file:" << _logFile.error() << _logFile.errorString();
228 _playbackStartTimeMSecs = 0;
229 _playbackStartLogTimeUSecs = 0;
230 _logCurrentTimeUSecs = _logStartTimeUSecs;
233void LogReplayWorker::_readNextLogEntry()
235 int timeToNextExecutionMSecs = 0;
236 while (timeToNextExecutionMSecs < 3) {
238 bytes.reserve(_logFile.bytesAvailable());
239 const qint64 nextTimeUSecs = _readNextMavlinkMessage(bytes);
241 emit
playbackPercentCompleteChanged((
static_cast<float>(_logCurrentTimeUSecs - _logStartTimeUSecs) /
static_cast<float>(_logDurationUSecs)) * 100);
243 if (_logFile.atEnd()) {
249 _logCurrentTimeUSecs = nextTimeUSecs;
251 const quint64 currentTimeMSecs =
static_cast<quint64
>(QDateTime::currentMSecsSinceEpoch());
252 const quint64 desiredPlayheadMovementTimeMSecs = ((_logCurrentTimeUSecs - _playbackStartLogTimeUSecs) / 1000) / _playbackSpeed;
253 const quint64 desiredCurrentTimeMSecs = _playbackStartTimeMSecs + desiredPlayheadMovementTimeMSecs;
254 timeToNextExecutionMSecs = desiredCurrentTimeMSecs - currentTimeMSecs;
257 _signalCurrentLogTimeSecs();
259 _readTickTimer->start(timeToNextExecutionMSecs);
262void LogReplayWorker::_signalCurrentLogTimeSecs()
267bool LogReplayWorker::_loadLogFile()
269 if (_logFile.isOpen()) {
271 emit
errorOccurred(tr(
"Attempt to load new log while log being played"));
275 const QString logFilename = _logReplayConfig->
logFilename();
276 _logFile.setFileName(logFilename);
277 if (!_logFile.open(QFile::ReadOnly)) {
278 emit
errorOccurred(tr(
"Unable to open log file: '%1', error: %2").arg(logFilename, _logFile.errorString()));
282 QFileInfo logFileInfo;
283 logFileInfo.setFile(logFilename);
284 _logFileSize = logFileInfo.size();
286 const quint64 startTimeUSecs = _parseTimestamp(_logFile.read(kTimestamp));
287 const quint64 endTimeUSecs = _findLastTimestamp();
288 if (endTimeUSecs <= startTimeUSecs) {
290 emit
errorOccurred(tr(
"The log file '%1' is corrupt or empty.").arg(logFilename));
294 _logEndTimeUSecs = endTimeUSecs;
295 _logStartTimeUSecs = startTimeUSecs;
296 _logDurationUSecs = endTimeUSecs - startTimeUSecs;
297 _logCurrentTimeUSecs = startTimeUSecs;
299 if (!_logFile.reset()) {
300 qCWarning(LogReplayLinkLog) <<
"failed to reset log file:" << _logFile.error() << _logFile.errorString();
303 const quint64 logDurationSecondsTotal = _logDurationUSecs / 1000000;
309quint64 LogReplayWorker::_parseTimestamp(
const QByteArray &bytes)
312 if (bytes.size() <
static_cast<qsizetype
>(
sizeof(quint64))) {
316 const quint64 currentTimestamp =
static_cast<quint64
>(QDateTime::currentMSecsSinceEpoch()) * 1000;
319 quint64 timestamp = qFromBigEndian<quint64>(bytes.constData());
320 if (timestamp > currentTimestamp) {
321 timestamp = qbswap(timestamp);
327quint64 LogReplayWorker::_readNextMavlinkMessage(QByteArray &bytes)
332 while (_logFile.getChar(&nextByte)) {
334 mavlink_status_t status{};
335 const bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, &message, &status);
337 if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
340 (void) bytes.append(nextByte);
343 const QByteArray rawTime = _logFile.read(kTimestamp);
344 return _parseTimestamp(rawTime);
353 mavlink_reset_channel_status(_mavlinkChannel);
355 qint64 messageStartPos = -1;
357 while (_logFile.getChar(&nextByte)) {
358 mavlink_status_t status{};
359 const bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, &nextMsg, &status);
361 if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
362 messageStartPos = _logFile.pos() - 1;
365 if (messageFound && (messageStartPos != -1)) {
366 if (!_logFile.seek(messageStartPos - kTimestamp)) {
367 qCWarning(LogReplayLinkLog) <<
"Failed to seek next message:" << _logFile.error() << _logFile.errorString();
371 const QByteArray rawTime = _logFile.read(kTimestamp);
372 return _parseTimestamp(rawTime);
379quint64 LogReplayWorker::_findLastTimestamp()
381 if (!_logFile.reset()) {
382 qCWarning(LogReplayLinkLog) <<
"failed to reset log file:" << _logFile.error() << _logFile.errorString();
385 mavlink_reset_channel_status(_mavlinkChannel);
387 quint64 lastTimestamp = 0;
389 while (_logFile.bytesAvailable() >=
static_cast<qint64
>(kTimestamp)) {
390 const quint64 candidateTimestamp = _parseTimestamp(_logFile.read(kTimestamp));
392 bool endOfMessage =
false;
394 while (!endOfMessage && _logFile.getChar(&nextByte)) {
396 mavlink_status_t status{};
397 endOfMessage = mavlink_parse_char(_mavlinkChannel, nextByte, &msg, &status);
401 if (lastTimestamp != 0) {
405 qCWarning(LogReplayLinkLog)
406 <<
"Ignoring trailing bytes at end of log file which do not form a complete MAVLink message";
408 qCWarning(LogReplayLinkLog) <<
"No complete MAVLink message found in log file";
413 lastTimestamp = candidateTimestamp;
416 return lastTimestamp;
425 , _workerThread(new QThread(this))
427 qCDebug(LogReplayLinkLog) <<
this;
429 _workerThread->setObjectName(QStringLiteral(
"LogReplay_%1").arg(_logReplayConfig->
name()));
431 _worker->moveToThread(_workerThread);
434 (void) connect(_workerThread, &QThread::finished, _worker, &QObject::deleteLater);
448 _workerThread->start();
454 (void) QMetaObject::invokeMethod(_worker,
"disconnectFromLog", Qt::BlockingQueuedConnection);
458 _workerThread->quit();
459 if (!_workerThread->wait()) {
460 qCWarning(LogReplayLinkLog) <<
"Failed to wait for LogReplay Thread to close";
463 qCDebug(LogReplayLinkLog) <<
this;
471bool LogReplayLink::_connect()
473 return QMetaObject::invokeMethod(_worker,
"connectToLog", Qt::QueuedConnection);
479 (void) QMetaObject::invokeMethod(_worker,
"disconnectFromLog", Qt::QueuedConnection);
483void LogReplayLink::_onConnected()
485 _disconnectedEmitted =
false;
489void LogReplayLink::_onDisconnected()
491 if (!_disconnectedEmitted.exchange(
true)) {
496void LogReplayLink::_onErrorOccurred(
const QString &
errorString)
498 qCWarning(LogReplayLinkLog) <<
"Error:" <<
errorString;
502void LogReplayLink::_onDataReceived(
const QByteArray &data)
514 (void) QMetaObject::invokeMethod(_worker,
"play", Qt::QueuedConnection);
519 (void) QMetaObject::invokeMethod(_worker,
"pause", Qt::QueuedConnection);
524 (void) QMetaObject::invokeMethod(_worker,
"setPlaybackSpeed", Qt::QueuedConnection, playbackSpeed);
529 (void) QMetaObject::invokeMethod(_worker,
"movePlayhead", Qt::QueuedConnection, percentComplete);
std::shared_ptr< LinkConfiguration > SharedLinkConfigurationPtr
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Interface holding link specific settings.
virtual void copyFrom(const LinkConfiguration *source)
The link interface defines the interface for all links used to communicate with the ground station ap...
void bytesReceived(LinkInterface *link, const QByteArray &data)
void communicationError(const QString &title, const QString &error)
void setConnectionsAllowed()
Sets the flag to allow new connections to be made.
static LinkManager * instance()
void setConnectionsSuspended(const QString &reason)
QString logFilenameShort() const
LogReplayConfiguration(const QString &name, QObject *parent=nullptr)
void copyFrom(const LinkConfiguration *source) override
void saveSettings(QSettings &settings, const QString &root) const override
void loadSettings(QSettings &settings, const QString &root) override
virtual ~LogReplayConfiguration()
QString logFilename() const
void setLogFilename(const QString &logFilename)
void disconnect() override
bool isConnected() const override
void playbackPercentCompleteChanged(qreal percentComplete)
void logFileStats(uint32_t logDurationSecs)
void currentLogTimeSecs(uint32_t secs)
void setPlaybackSpeed(qreal playbackSpeed)
void movePlayhead(qreal percentComplete)
LogReplayLink(SharedLinkConfigurationPtr &config, QObject *parent=nullptr)
void currentLogTimeSecs(uint32_t secs)
void dataReceived(const QByteArray &data)
void setPlaybackSpeed(qreal playbackSpeed)
void logFileStats(uint32_t logDurationSecs)
void errorOccurred(const QString &errorString)
void movePlayhead(qreal percentComplete)
LogReplayWorker(const LogReplayConfiguration *config, QObject *parent=nullptr)
void playbackPercentCompleteChanged(qreal percentComplete)
static MAVLinkProtocol * instance()
void suspendLogForReplay(bool suspend)
static MultiVehicleManager * instance()