QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
LogReplayLink.cc
Go to the documentation of this file.
1#include "LogReplayLink.h"
2#include "LinkManager.h"
3#include "MAVLinkLib.h"
4#include "MAVLinkProtocol.h"
7
8#include <QtCore/QFileInfo>
9#include <QtCore/QtEndian>
10#include <QtCore/QThread>
11#include <QtCore/QTimer>
12
13QGC_LOGGING_CATEGORY(LogReplayLinkLog, "Comms.LogReplayLink")
14
15/*===========================================================================*/
16
17LogReplayConfiguration::LogReplayConfiguration(const QString &name, QObject *parent)
18 : LinkConfiguration(name, parent)
19{
20 qCDebug(LogReplayLinkLog) << this;
21}
22
24 : LinkConfiguration(copy, parent)
25 , _logFilename(copy->logFilename())
26{
27 qCDebug(LogReplayLinkLog) << this;
28}
29
31{
32 qCDebug(LogReplayLinkLog) << this;
33}
34
36{
38
39 const LogReplayConfiguration *logReplaySource = qobject_cast<const LogReplayConfiguration*>(source);
40
41 setLogFilename(logReplaySource->logFilename());
42}
43
44void LogReplayConfiguration::loadSettings(QSettings &settings, const QString &root)
45{
46 settings.beginGroup(root);
47
48 setLogFilename(settings.value("logFilename", "").toString());
49
50 settings.endGroup();
51}
52
53void LogReplayConfiguration::saveSettings(QSettings &settings, const QString &root) const
54{
55 settings.beginGroup(root);
56
57 settings.setValue("logFilename", _logFilename);
58
59 settings.endGroup();
60}
61
63{
64 return QFileInfo(_logFilename).fileName();
65}
66
67void LogReplayConfiguration::setLogFilename(const QString &logFilename)
68{
69 if (logFilename != _logFilename) {
70 _logFilename = logFilename;
71 emit filenameChanged();
72 }
73}
74
75/*===========================================================================*/
76
78 : QObject(parent)
79 , _logReplayConfig(config)
80{
81 qCDebug(LogReplayLinkLog) << this;
82}
83
85{
87
88 qCDebug(LogReplayLinkLog) << this;
89}
90
92{
93 if (!_readTickTimer) {
94 _readTickTimer = new QTimer(this);
95 }
96
97 (void) connect(_readTickTimer, &QTimer::timeout, this, &LogReplayWorker::_readNextLogEntry);
98}
99
101{
102 if (isConnected()) {
103 qCWarning(LogReplayLinkLog) << "Already connected";
104 return;
105 }
106
107 if (MultiVehicleManager::instance()->activeVehicle()) {
108 emit errorOccurred(tr("You must close all connections prior to replaying a log."));
109 return;
110 }
111
112 if (!_loadLogFile()) {
114 return;
115 }
116
117 _isConnected = true;
118 emit connected();
119
120 play();
121}
122
124{
125 if (!isConnected()) {
126 qCDebug(LogReplayLinkLog) << "Already disconnected";
127 return;
128 }
129
130 qCDebug(LogReplayLinkLog) << "Disconnecting from log";
131
132 if (_readTickTimer) {
133 _readTickTimer->stop();
134 }
135
136 if (_logFile.isOpen()) {
137 _logFile.close();
138 }
139
140 _isConnected = false;
141 emit disconnected();
142}
143
145{
146 return (_readTickTimer && _readTickTimer->isActive());
147}
148
150{
151 LinkManager::instance()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay."));
153
154 if (_logFile.atEnd()) {
155 _resetPlaybackToBeginning();
156 }
157
158 _playbackStartTimeMSecs = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch());
159 _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
160 _readTickTimer->start(1);
161
162 emit playbackStarted();
163}
164
166{
169
170 _readTickTimer->stop();
171
172 emit playbackPaused();
173}
174
175void LogReplayWorker::setPlaybackSpeed(qreal playbackSpeed)
176{
177 _playbackSpeed = playbackSpeed;
178 _playbackStartTimeMSecs = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch());
179 _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
180 _readTickTimer->start(1);
181}
182
183void LogReplayWorker::movePlayhead(qreal percentComplete)
184{
185 if (isPlaying()) {
186 pause();
187 if (_readTickTimer->isActive()) {
188 return;
189 }
190 }
191
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)) {
196 emit errorOccurred(tr("Unable to seek to new position"));
197 return;
198 }
199
200 mavlink_message_t dummy{};
201 _logCurrentTimeUSecs = _seekToNextMavlinkMessage(dummy);
202
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)) {
208 emit errorOccurred(tr("Unable to seek to new position"));
209 return;
210 }
211
212 _logCurrentTimeUSecs = _seekToNextMavlinkMessage(dummy);
213 _signalCurrentLogTimeSecs();
214
215 newRelativeTimeUSecs = static_cast<qreal>(_logCurrentTimeUSecs - _logStartTimeUSecs);
216 percentComplete = ((newRelativeTimeUSecs / _logDurationUSecs) * 100);
217 emit playbackPercentCompleteChanged(percentComplete);
218}
219
220void LogReplayWorker::_resetPlaybackToBeginning()
221{
222 if (_logFile.isOpen()) {
223 if (!_logFile.reset()) {
224 qCWarning(LogReplayLinkLog) << "failed to reset log file:" << _logFile.error() << _logFile.errorString();
225 }
226 }
227
228 _playbackStartTimeMSecs = 0;
229 _playbackStartLogTimeUSecs = 0;
230 _logCurrentTimeUSecs = _logStartTimeUSecs;
231}
232
233void LogReplayWorker::_readNextLogEntry()
234{
235 int timeToNextExecutionMSecs = 0;
236 while (timeToNextExecutionMSecs < 3) {
237 QByteArray bytes;
238 bytes.reserve(_logFile.bytesAvailable());
239 const qint64 nextTimeUSecs = _readNextMavlinkMessage(bytes);
240 emit dataReceived(bytes);
241 emit playbackPercentCompleteChanged((static_cast<float>(_logCurrentTimeUSecs - _logStartTimeUSecs) / static_cast<float>(_logDurationUSecs)) * 100);
242
243 if (_logFile.atEnd()) {
244 pause();
245 emit playbackAtEnd();
246 return;
247 }
248
249 _logCurrentTimeUSecs = nextTimeUSecs;
250
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;
255 }
256
257 _signalCurrentLogTimeSecs();
258
259 _readTickTimer->start(timeToNextExecutionMSecs);
260}
261
262void LogReplayWorker::_signalCurrentLogTimeSecs()
263{
264 emit currentLogTimeSecs((_logCurrentTimeUSecs - _logStartTimeUSecs) / 1000000);
265}
266
267bool LogReplayWorker::_loadLogFile()
268{
269 if (_logFile.isOpen()) {
270 _logFile.close();
271 emit errorOccurred(tr("Attempt to load new log while log being played"));
272 return false;
273 }
274
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()));
279 return false;
280 }
281
282 QFileInfo logFileInfo;
283 logFileInfo.setFile(logFilename);
284 _logFileSize = logFileInfo.size();
285
286 const quint64 startTimeUSecs = _parseTimestamp(_logFile.read(kTimestamp));
287 const quint64 endTimeUSecs = _findLastTimestamp();
288 if (endTimeUSecs <= startTimeUSecs) {
289 _logFile.close();
290 emit errorOccurred(tr("The log file '%1' is corrupt or empty.").arg(logFilename));
291 return false;
292 }
293
294 _logEndTimeUSecs = endTimeUSecs;
295 _logStartTimeUSecs = startTimeUSecs;
296 _logDurationUSecs = endTimeUSecs - startTimeUSecs;
297 _logCurrentTimeUSecs = startTimeUSecs;
298
299 if (!_logFile.reset()) {
300 qCWarning(LogReplayLinkLog) << "failed to reset log file:" << _logFile.error() << _logFile.errorString();
301 }
302
303 const quint64 logDurationSecondsTotal = _logDurationUSecs / 1000000;
304 emit logFileStats(logDurationSecondsTotal);
305
306 return true;
307}
308
309quint64 LogReplayWorker::_parseTimestamp(const QByteArray &bytes)
310{
311 const quint64 currentTimestamp = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch()) * 1000;
312 quint64 timestamp = qFromBigEndian(*reinterpret_cast<const quint64*>(bytes.constData()));
313 if (timestamp > currentTimestamp) {
314 timestamp = qbswap(timestamp);
315 }
316
317 return timestamp;
318}
319
320quint64 LogReplayWorker::_readNextMavlinkMessage(QByteArray &bytes)
321{
322 bytes.clear();
323
324 char nextByte;
325 while (_logFile.getChar(&nextByte)) {
326 mavlink_message_t message{};
327 mavlink_status_t status{};
328 const bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, &message, &status);
329
330 if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
331 bytes.clear();
332 }
333 (void) bytes.append(nextByte);
334
335 if (messageFound) {
336 const QByteArray rawTime = _logFile.read(kTimestamp);
337 return _parseTimestamp(rawTime);
338 }
339 }
340
341 return 0;
342}
343
344quint64 LogReplayWorker::_seekToNextMavlinkMessage(mavlink_message_t &nextMsg)
345{
346 mavlink_reset_channel_status(_mavlinkChannel);
347
348 qint64 messageStartPos = -1;
349 char nextByte;
350 while (_logFile.getChar(&nextByte)) {
351 mavlink_status_t status{};
352 const bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, &nextMsg, &status);
353
354 if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
355 messageStartPos = _logFile.pos() - 1;
356 }
357
358 if (messageFound && (messageStartPos != -1)) {
359 if (!_logFile.seek(messageStartPos - kTimestamp)) {
360 qCWarning(LogReplayLinkLog) << "Failed to seek next message:" << _logFile.error() << _logFile.errorString();
361 break;
362 }
363
364 const QByteArray rawTime = _logFile.read(kTimestamp);
365 return _parseTimestamp(rawTime);
366 }
367 }
368
369 return 0;
370}
371
372quint64 LogReplayWorker::_findLastTimestamp()
373{
374 if (!_logFile.reset()) {
375 qCWarning(LogReplayLinkLog) << "failed to reset log file:" << _logFile.error() << _logFile.errorString();
376 }
377
378 mavlink_reset_channel_status(_mavlinkChannel);
379
380 quint64 lastTimestamp = 0;
381
382 while (_logFile.bytesAvailable() > static_cast<qint64>(kTimestamp)) {
383 lastTimestamp = _parseTimestamp(_logFile.read(kTimestamp));
384
385 bool endOfMessage = false;
386 char nextByte;
387 while (!endOfMessage && _logFile.getChar(&nextByte)) {
388 mavlink_message_t msg{};
389 mavlink_status_t status{};
390 endOfMessage = mavlink_parse_char(_mavlinkChannel, nextByte, &msg, &status);
391 }
392 }
393
394 return lastTimestamp;
395}
396
397/*===========================================================================*/
398
400 : LinkInterface(config, parent)
401 , _logReplayConfig(qobject_cast<LogReplayConfiguration*>(config.get()))
402 , _worker(new LogReplayWorker(_logReplayConfig))
403 , _workerThread(new QThread(this))
404{
405 qCDebug(LogReplayLinkLog) << this;
406
407 _workerThread->setObjectName(QStringLiteral("LogReplay_%1").arg(_logReplayConfig->name()));
408
409 _worker->moveToThread(_workerThread);
410
411 (void) connect(_workerThread, &QThread::started, _worker, &LogReplayWorker::setup);
412 (void) connect(_workerThread, &QThread::finished, _worker, &QObject::deleteLater);
413
414 (void) connect(_worker, &LogReplayWorker::connected, this, &LogReplayLink::_onConnected, Qt::QueuedConnection);
415 (void) connect(_worker, &LogReplayWorker::disconnected, this, &LogReplayLink::_onDisconnected, Qt::QueuedConnection);
416 (void) connect(_worker, &LogReplayWorker::errorOccurred, this, &LogReplayLink::_onErrorOccurred, Qt::QueuedConnection);
417 (void) connect(_worker, &LogReplayWorker::dataReceived, this, &LogReplayLink::_onDataReceived, Qt::QueuedConnection);
418
419 (void) connect(_worker, &LogReplayWorker::logFileStats, this, &LogReplayLink::logFileStats, Qt::QueuedConnection);
420 (void) connect(_worker, &LogReplayWorker::playbackStarted, this, &LogReplayLink::playbackStarted, Qt::QueuedConnection);
421 (void) connect(_worker, &LogReplayWorker::playbackPaused, this, &LogReplayLink::playbackPaused, Qt::QueuedConnection);
422 (void) connect(_worker, &LogReplayWorker::playbackPercentCompleteChanged, this, &LogReplayLink::playbackPercentCompleteChanged, Qt::QueuedConnection);
423 (void) connect(_worker, &LogReplayWorker::currentLogTimeSecs, this, &LogReplayLink::currentLogTimeSecs, Qt::QueuedConnection);
424 (void) connect(_worker, &LogReplayWorker::disconnected, this, &LogReplayLink::disconnected, Qt::QueuedConnection);
425
426 _workerThread->start();
427}
428
430{
431 if (isConnected()) {
432 (void) QMetaObject::invokeMethod(_worker, "disconnectFromLog", Qt::BlockingQueuedConnection);
433 _onDisconnected();
434 }
435
436 _workerThread->quit();
437 if (!_workerThread->wait()) {
438 qCWarning(LogReplayLinkLog) << "Failed to wait for LogReplay Thread to close";
439 }
440
441 qCDebug(LogReplayLinkLog) << this;
442}
443
445{
446 return _worker && _worker->isConnected();
447}
448
449bool LogReplayLink::_connect()
450{
451 return QMetaObject::invokeMethod(_worker, "connectToLog", Qt::QueuedConnection);
452}
453
455{
456 if (isConnected()) {
457 (void) QMetaObject::invokeMethod(_worker, "disconnectFromLog", Qt::QueuedConnection);
458 }
459}
460
461void LogReplayLink::_onConnected()
462{
463 _disconnectedEmitted = false;
464 emit connected();
465}
466
467void LogReplayLink::_onDisconnected()
468{
469 if (!_disconnectedEmitted.exchange(true)) {
470 emit disconnected();
471 }
472}
473
474void LogReplayLink::_onErrorOccurred(const QString &errorString)
475{
476 qCWarning(LogReplayLinkLog) << "Error:" << errorString;
477 emit communicationError(tr("Log Replay Link Error"), tr("Link: %1, %2.").arg(_logReplayConfig->name(), errorString));
478}
479
480void LogReplayLink::_onDataReceived(const QByteArray &data)
481{
482 emit bytesReceived(this, data);
483}
484
486{
487 return _worker && _worker->isPlaying();
488}
489
491{
492 (void) QMetaObject::invokeMethod(_worker, "play", Qt::QueuedConnection);
493}
494
496{
497 (void) QMetaObject::invokeMethod(_worker, "pause", Qt::QueuedConnection);
498}
499
500void LogReplayLink::setPlaybackSpeed(qreal playbackSpeed)
501{
502 (void) QMetaObject::invokeMethod(_worker, "setPlaybackSpeed", Qt::QueuedConnection, playbackSpeed);
503}
504
505void LogReplayLink::movePlayhead(qreal percentComplete)
506{
507 (void) QMetaObject::invokeMethod(_worker, "movePlayhead", Qt::QueuedConnection, percentComplete);
508}
std::shared_ptr< LinkConfiguration > SharedLinkConfigurationPtr
QString errorString
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Interface holding link specific settings.
virtual void copyFrom(const LinkConfiguration *source)
QString name() const
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 disconnected()
void communicationError(const QString &title, const QString &error)
void connected()
void setConnectionsAllowed()
Sets the flag to allow new connections to be made.
Definition LinkManager.h:77
static LinkManager * instance()
void setConnectionsSuspended(const QString &reason)
Definition LinkManager.h:74
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 currentLogTimeSecs(uint32_t secs)
void dataReceived(const QByteArray &data)
void setPlaybackSpeed(qreal playbackSpeed)
void logFileStats(uint32_t logDurationSecs)
void errorOccurred(const QString &errorString)
bool isPlaying() const
void movePlayhead(qreal percentComplete)
void playbackPaused()
bool isConnected() const
LogReplayWorker(const LogReplayConfiguration *config, QObject *parent=nullptr)
void playbackAtEnd()
void playbackStarted()
void playbackPercentCompleteChanged(qreal percentComplete)
static MAVLinkProtocol * instance()
void suspendLogForReplay(bool suspend)
static MultiVehicleManager * instance()