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{
37 const LogReplayConfiguration *logReplaySource = qobject_cast<const LogReplayConfiguration*>(source);
38 if (!logReplaySource) {
39 qCWarning(LogReplayLinkLog) << "Invalid source configuration type";
40 return;
41 }
42
44
45 setLogFilename(logReplaySource->logFilename());
46}
47
48void LogReplayConfiguration::loadSettings(QSettings &settings, const QString &root)
49{
50 settings.beginGroup(root);
51
52 setLogFilename(settings.value("logFilename", "").toString());
53
54 settings.endGroup();
55}
56
57void LogReplayConfiguration::saveSettings(QSettings &settings, const QString &root) const
58{
59 settings.beginGroup(root);
60
61 settings.setValue("logFilename", _logFilename);
62
63 settings.endGroup();
64}
65
67{
68 return QFileInfo(_logFilename).fileName();
69}
70
71void LogReplayConfiguration::setLogFilename(const QString &logFilename)
72{
73 if (logFilename != _logFilename) {
74 _logFilename = logFilename;
75 emit filenameChanged();
76 }
77}
78
79/*===========================================================================*/
80
82 : QObject(parent)
83 , _logReplayConfig(config)
84{
85 qCDebug(LogReplayLinkLog) << this;
86}
87
89{
91
92 qCDebug(LogReplayLinkLog) << this;
93}
94
96{
97 if (!_readTickTimer) {
98 _readTickTimer = new QTimer(this);
99 }
100
101 (void) connect(_readTickTimer, &QTimer::timeout, this, &LogReplayWorker::_readNextLogEntry);
102}
103
105{
106 if (isConnected()) {
107 qCWarning(LogReplayLinkLog) << "Already connected";
108 return;
109 }
110
111 if (MultiVehicleManager::instance()->activeVehicle()) {
112 emit errorOccurred(tr("You must close all connections prior to replaying a log."));
113 return;
114 }
115
116 if (!_loadLogFile()) {
118 return;
119 }
120
121 _isConnected = true;
122 emit connected();
123
124 play();
125}
126
128{
129 if (!isConnected()) {
130 qCDebug(LogReplayLinkLog) << "Already disconnected";
131 return;
132 }
133
134 qCDebug(LogReplayLinkLog) << "Disconnecting from log";
135
136 if (_readTickTimer) {
137 _readTickTimer->stop();
138 }
139
140 if (_logFile.isOpen()) {
141 _logFile.close();
142 }
143
144 _isConnected = false;
145 emit disconnected();
146}
147
149{
150 return (_readTickTimer && _readTickTimer->isActive());
151}
152
154{
155 LinkManager::instance()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay."));
157
158 if (_logFile.atEnd()) {
159 _resetPlaybackToBeginning();
160 }
161
162 _playbackStartTimeMSecs = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch());
163 _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
164 _readTickTimer->start(1);
165
166 emit playbackStarted();
167}
168
170{
173
174 _readTickTimer->stop();
175
176 emit playbackPaused();
177}
178
179void LogReplayWorker::setPlaybackSpeed(qreal playbackSpeed)
180{
181 _playbackSpeed = playbackSpeed;
182 _playbackStartTimeMSecs = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch());
183 _playbackStartLogTimeUSecs = _logCurrentTimeUSecs;
184 _readTickTimer->start(1);
185}
186
187void LogReplayWorker::movePlayhead(qreal percentComplete)
188{
189 if (isPlaying()) {
190 pause();
191 if (_readTickTimer->isActive()) {
192 return;
193 }
194 }
195
196 percentComplete = qBound(0., percentComplete, 100.);
197 const qreal percentCompleteMult = percentComplete / 100.0;
198 const qint64 newFilePos = static_cast<qint64>(percentCompleteMult * static_cast<qreal>(_logFile.size()));
199 if (!_logFile.seek(newFilePos)) {
200 emit errorOccurred(tr("Unable to seek to new position"));
201 return;
202 }
203
204 mavlink_message_t dummy{};
205 _logCurrentTimeUSecs = _seekToNextMavlinkMessage(dummy);
206
207 qreal newRelativeTimeUSecs = static_cast<qreal>(_logCurrentTimeUSecs - _logStartTimeUSecs);
208 const qreal baudRate = _logFile.size() / static_cast<qreal>(_logDurationUSecs) / 1e6;
209 const qreal desiredTimeUSecs = percentCompleteMult * _logDurationUSecs;
210 const qint64 offset = (newRelativeTimeUSecs - desiredTimeUSecs) * baudRate;
211 if (!_logFile.seek(_logFile.pos() + offset)) {
212 emit errorOccurred(tr("Unable to seek to new position"));
213 return;
214 }
215
216 _logCurrentTimeUSecs = _seekToNextMavlinkMessage(dummy);
217 _signalCurrentLogTimeSecs();
218
219 newRelativeTimeUSecs = static_cast<qreal>(_logCurrentTimeUSecs - _logStartTimeUSecs);
220 percentComplete = ((newRelativeTimeUSecs / _logDurationUSecs) * 100);
221 emit playbackPercentCompleteChanged(percentComplete);
222}
223
224void LogReplayWorker::_resetPlaybackToBeginning()
225{
226 if (_logFile.isOpen()) {
227 if (!_logFile.reset()) {
228 qCWarning(LogReplayLinkLog) << "failed to reset log file:" << _logFile.error() << _logFile.errorString();
229 }
230 }
231
232 _playbackStartTimeMSecs = 0;
233 _playbackStartLogTimeUSecs = 0;
234 _logCurrentTimeUSecs = _logStartTimeUSecs;
235}
236
237void LogReplayWorker::_readNextLogEntry()
238{
239 int timeToNextExecutionMSecs = 0;
240 while (timeToNextExecutionMSecs < 3) {
241 QByteArray bytes;
242 bytes.reserve(_logFile.bytesAvailable());
243 const qint64 nextTimeUSecs = _readNextMavlinkMessage(bytes);
244 emit dataReceived(bytes);
245 emit playbackPercentCompleteChanged((static_cast<float>(_logCurrentTimeUSecs - _logStartTimeUSecs) / static_cast<float>(_logDurationUSecs)) * 100);
246
247 if (_logFile.atEnd()) {
248 pause();
249 emit playbackAtEnd();
250 return;
251 }
252
253 _logCurrentTimeUSecs = nextTimeUSecs;
254
255 const quint64 currentTimeMSecs = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch());
256 const quint64 desiredPlayheadMovementTimeMSecs = ((_logCurrentTimeUSecs - _playbackStartLogTimeUSecs) / 1000) / _playbackSpeed;
257 const quint64 desiredCurrentTimeMSecs = _playbackStartTimeMSecs + desiredPlayheadMovementTimeMSecs;
258 timeToNextExecutionMSecs = desiredCurrentTimeMSecs - currentTimeMSecs;
259 }
260
261 _signalCurrentLogTimeSecs();
262
263 _readTickTimer->start(timeToNextExecutionMSecs);
264}
265
266void LogReplayWorker::_signalCurrentLogTimeSecs()
267{
268 emit currentLogTimeSecs((_logCurrentTimeUSecs - _logStartTimeUSecs) / 1000000);
269}
270
271bool LogReplayWorker::_loadLogFile()
272{
273 if (_logFile.isOpen()) {
274 _logFile.close();
275 emit errorOccurred(tr("Attempt to load new log while log being played"));
276 return false;
277 }
278
279 const QString logFilename = _logReplayConfig->logFilename();
280 _logFile.setFileName(logFilename);
281 if (!_logFile.open(QFile::ReadOnly)) {
282 emit errorOccurred(tr("Unable to open log file: '%1', error: %2").arg(logFilename, _logFile.errorString()));
283 return false;
284 }
285
286 QFileInfo logFileInfo;
287 logFileInfo.setFile(logFilename);
288 _logFileSize = logFileInfo.size();
289
290 const quint64 startTimeUSecs = _parseTimestamp(_logFile.read(kTimestamp));
291 const quint64 endTimeUSecs = _findLastTimestamp();
292 if (endTimeUSecs <= startTimeUSecs) {
293 _logFile.close();
294 emit errorOccurred(tr("The log file '%1' is corrupt or empty.").arg(logFilename));
295 return false;
296 }
297
298 _logEndTimeUSecs = endTimeUSecs;
299 _logStartTimeUSecs = startTimeUSecs;
300 _logDurationUSecs = endTimeUSecs - startTimeUSecs;
301 _logCurrentTimeUSecs = startTimeUSecs;
302
303 if (!_logFile.reset()) {
304 qCWarning(LogReplayLinkLog) << "failed to reset log file:" << _logFile.error() << _logFile.errorString();
305 }
306
307 const quint64 logDurationSecondsTotal = _logDurationUSecs / 1000000;
308 emit logFileStats(logDurationSecondsTotal);
309
310 return true;
311}
312
313quint64 LogReplayWorker::_parseTimestamp(const QByteArray &bytes)
314{
315 // Truncated log files can produce a short read; never read past the buffer.
316 if (bytes.size() < static_cast<qsizetype>(sizeof(quint64))) {
317 return 0;
318 }
319
320 const quint64 currentTimestamp = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch()) * 1000;
321 // qFromBigEndian(const void *src) handles unaligned reads; dereferencing a
322 // cast pointer here would be a misaligned load (UB).
323 quint64 timestamp = qFromBigEndian<quint64>(bytes.constData());
324 if (timestamp > currentTimestamp) {
325 timestamp = qbswap(timestamp);
326 }
327
328 return timestamp;
329}
330
331quint64 LogReplayWorker::_readNextMavlinkMessage(QByteArray &bytes)
332{
333 bytes.clear();
334
335 char nextByte;
336 while (_logFile.getChar(&nextByte)) {
337 mavlink_message_t message{};
338 mavlink_status_t status{};
339 const bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, &message, &status);
340
341 if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
342 bytes.clear();
343 }
344 (void) bytes.append(nextByte);
345
346 if (messageFound) {
347 const QByteArray rawTime = _logFile.read(kTimestamp);
348 return _parseTimestamp(rawTime);
349 }
350 }
351
352 return 0;
353}
354
355quint64 LogReplayWorker::_seekToNextMavlinkMessage(mavlink_message_t &nextMsg)
356{
357 mavlink_reset_channel_status(_mavlinkChannel);
358
359 qint64 messageStartPos = -1;
360 char nextByte;
361 while (_logFile.getChar(&nextByte)) {
362 mavlink_status_t status{};
363 const bool messageFound = mavlink_parse_char(_mavlinkChannel, nextByte, &nextMsg, &status);
364
365 if (status.parse_state == MAVLINK_PARSE_STATE_GOT_STX) {
366 messageStartPos = _logFile.pos() - 1;
367 }
368
369 if (messageFound && (messageStartPos != -1)) {
370 if (!_logFile.seek(messageStartPos - kTimestamp)) {
371 qCWarning(LogReplayLinkLog) << "Failed to seek next message:" << _logFile.error() << _logFile.errorString();
372 break;
373 }
374
375 const QByteArray rawTime = _logFile.read(kTimestamp);
376 return _parseTimestamp(rawTime);
377 }
378 }
379
380 return 0;
381}
382
383quint64 LogReplayWorker::_findLastTimestamp()
384{
385 if (!_logFile.reset()) {
386 qCWarning(LogReplayLinkLog) << "failed to reset log file:" << _logFile.error() << _logFile.errorString();
387 }
388
389 mavlink_reset_channel_status(_mavlinkChannel);
390
391 quint64 lastTimestamp = 0;
392
393 while (_logFile.bytesAvailable() >= static_cast<qint64>(kTimestamp)) {
394 const quint64 candidateTimestamp = _parseTimestamp(_logFile.read(kTimestamp));
395
396 bool endOfMessage = false;
397 char nextByte;
398 while (!endOfMessage && _logFile.getChar(&nextByte)) {
399 mavlink_message_t msg{};
400 mavlink_status_t status{};
401 endOfMessage = mavlink_parse_char(_mavlinkChannel, nextByte, &msg, &status);
402 }
403
404 if (!endOfMessage) {
405 if (lastTimestamp != 0) {
406 // Trailing bytes which don't form a complete MAVLink message (e.g. the log was not
407 // closed cleanly due to a crash or power loss). Ignore them and keep the timestamp
408 // of the last complete message.
409 qCWarning(LogReplayLinkLog)
410 << "Ignoring trailing bytes at end of log file which do not form a complete MAVLink message";
411 } else {
412 qCWarning(LogReplayLinkLog) << "No complete MAVLink message found in log file";
413 }
414 break;
415 }
416
417 lastTimestamp = candidateTimestamp;
418 }
419
420 return lastTimestamp;
421}
422
423/*===========================================================================*/
424
426 : LinkInterface(config, parent)
427 , _logReplayConfig(qobject_cast<LogReplayConfiguration*>(config.get()))
428 , _worker(new LogReplayWorker(_logReplayConfig))
429 , _workerThread(new QThread(this))
430{
431 qCDebug(LogReplayLinkLog) << this;
432
433 _workerThread->setObjectName(QStringLiteral("LogReplay_%1").arg(_logReplayConfig->name()));
434
435 _worker->moveToThread(_workerThread);
436
437 (void) connect(_workerThread, &QThread::started, _worker, &LogReplayWorker::setup);
438 (void) connect(_workerThread, &QThread::finished, _worker, &QObject::deleteLater);
439
440 (void) connect(_worker, &LogReplayWorker::connected, this, &LogReplayLink::_onConnected, Qt::QueuedConnection);
441 (void) connect(_worker, &LogReplayWorker::disconnected, this, &LogReplayLink::_onDisconnected, Qt::QueuedConnection);
442 (void) connect(_worker, &LogReplayWorker::errorOccurred, this, &LogReplayLink::_onErrorOccurred, Qt::QueuedConnection);
443 (void) connect(_worker, &LogReplayWorker::dataReceived, this, &LogReplayLink::_onDataReceived, Qt::QueuedConnection);
444
445 (void) connect(_worker, &LogReplayWorker::logFileStats, this, &LogReplayLink::logFileStats, Qt::QueuedConnection);
446 (void) connect(_worker, &LogReplayWorker::playbackStarted, this, &LogReplayLink::playbackStarted, Qt::QueuedConnection);
447 (void) connect(_worker, &LogReplayWorker::playbackPaused, this, &LogReplayLink::playbackPaused, Qt::QueuedConnection);
448 (void) connect(_worker, &LogReplayWorker::playbackPercentCompleteChanged, this, &LogReplayLink::playbackPercentCompleteChanged, Qt::QueuedConnection);
449 (void) connect(_worker, &LogReplayWorker::currentLogTimeSecs, this, &LogReplayLink::currentLogTimeSecs, Qt::QueuedConnection);
450 (void) connect(_worker, &LogReplayWorker::disconnected, this, &LogReplayLink::disconnected, Qt::QueuedConnection);
451
452 _workerThread->start();
453}
454
456{
457 if (isConnected()) {
458 // Queued, not blocking: keeps a wedged worker from hanging the destructor before
459 // _shutdownWorkerThread can bound the wait. If quit() beats the queued call,
460 // ~LogReplayWorker still disconnects on thread finish.
461 (void) QMetaObject::invokeMethod(_worker, "disconnectFromLog", Qt::QueuedConnection);
462 _onDisconnected();
463 }
464
465 _shutdownWorkerThread(_workerThread, LogReplayLinkLog());
466
467 qCDebug(LogReplayLinkLog) << this;
468}
469
471{
472 return _worker && _worker->isConnected();
473}
474
475bool LogReplayLink::_connect()
476{
477 return QMetaObject::invokeMethod(_worker, "connectToLog", Qt::QueuedConnection);
478}
479
481{
482 if (isConnected()) {
483 (void) QMetaObject::invokeMethod(_worker, "disconnectFromLog", Qt::QueuedConnection);
484 }
485}
486
487void LogReplayLink::_onConnected()
488{
489 _disconnectedEmitted = false;
490 emit connected();
491}
492
493void LogReplayLink::_onDisconnected()
494{
495 if (!_disconnectedEmitted.exchange(true)) {
496 emit disconnected();
497 }
498}
499
500void LogReplayLink::_onErrorOccurred(const QString &errorString)
501{
502 qCWarning(LogReplayLinkLog) << "Error:" << errorString;
503 emit communicationError(tr("Log Replay Link Error"), tr("Link: %1, %2.").arg(_logReplayConfig->name(), errorString));
504}
505
506void LogReplayLink::_onDataReceived(const QByteArray &data)
507{
508 emit bytesReceived(this, data);
509}
510
512{
513 return _worker && _worker->isPlaying();
514}
515
517{
518 (void) QMetaObject::invokeMethod(_worker, "play", Qt::QueuedConnection);
519}
520
522{
523 (void) QMetaObject::invokeMethod(_worker, "pause", Qt::QueuedConnection);
524}
525
526void LogReplayLink::setPlaybackSpeed(qreal playbackSpeed)
527{
528 (void) QMetaObject::invokeMethod(_worker, "setPlaybackSpeed", Qt::QueuedConnection, playbackSpeed);
529}
530
531void LogReplayLink::movePlayhead(qreal percentComplete)
532{
533 (void) QMetaObject::invokeMethod(_worker, "movePlayhead", Qt::QueuedConnection, percentComplete);
534}
Config config
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 _shutdownWorkerThread(QThread *thread, const QLoggingCategory &category, bool allowTerminate=true)
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:80
static LinkManager * instance()
void setConnectionsSuspended(const QString &reason)
Definition LinkManager.h:77
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()