2 ******************************************************************************
5 * @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2017-2018.
6 * The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
7 * @see The GNU Public License (GPL) Version 3
9 *****************************************************************************/
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 3 of the License, or
14 * (at your option) any later version.
16 * This program is distributed in the hope that it will be useful, but
17 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
18 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21 * You should have received a copy of the GNU General Public License along
22 * with this program; if not, write to the Free Software Foundation, Inc.,
23 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
29 #include <QDataStream>
31 #define TIMESTAMP_SIZE_BYTES 4
33 LogFile::LogFile(QObject
*parent
) : QIODevice(parent
),
35 m_previousTimeStamp(0),
40 m_replayState(STOPPED
),
41 m_useProvidedTimeStamp(false),
42 m_providedTimeStamp(0),
47 connect(&m_timer
, &QTimer::timeout
, this, &LogFile::timerFired
);
50 bool LogFile::isSequential() const
52 // returning true fixes "UAVTalk - error : bad type" errors when replaying a log file
57 * Opens the logfile QIODevice and the underlying logfile. In case
58 * we want to save the logfile, we open in WriteOnly. In case we
59 * want to read the logfile, we open in ReadOnly.
61 bool LogFile::open(OpenMode mode
)
63 // start a timer for playback
65 if (m_file
.isOpen()) {
66 // We end up here when doing a replay, because the connection
67 // manager will also try to open the QIODevice, even though we just
68 // opened it after selecting the file, which happens before the
69 // connection manager call...
72 qDebug() << "LogFile - open" << fileName();
74 if (m_file
.open(mode
) == false) {
75 qWarning() << "Unable to open " << m_file
.fileName() << " for logging";
79 // TODO: Write a header at the beginng describing objects so that in future
80 // they can be read back if ID's change
82 // Must call parent function for QIODevice to pass calls to writeData
83 // We always open ReadWrite, because otherwise we will get tons of warnings
84 // during a logfile replay. Read nature is checked upon write ops below.
85 QIODevice::open(QIODevice::ReadWrite
);
92 qDebug() << "LogFile - close" << fileName();
98 qint64
LogFile::writeData(const char *data
, qint64 dataSize
)
100 if (!m_file
.isWritable()) {
104 // If needed, use provided timestamp instead of the GCS timer
105 // This is used when saving logs from on-board logging
106 quint32 timeStamp
= m_useProvidedTimeStamp
? m_providedTimeStamp
: m_myTime
.elapsed();
108 m_file
.write((char *)&timeStamp
, sizeof(timeStamp
));
109 m_file
.write((char *)&dataSize
, sizeof(dataSize
));
111 qint64 written
= m_file
.write(data
, dataSize
);
113 // flush (needed to avoid UAVTalk device full errors)
117 emit
bytesWritten(written
);
123 qint64
LogFile::readData(char *data
, qint64 maxlen
)
125 QMutexLocker
locker(&m_mutex
);
127 qint64 len
= qMin(maxlen
, (qint64
)m_dataBuffer
.size());
130 memcpy(data
, m_dataBuffer
.data(), len
);
131 m_dataBuffer
.remove(0, len
);
137 qint64
LogFile::bytesAvailable() const
139 QMutexLocker
locker(&m_mutex
);
141 qint64 len
= m_dataBuffer
.size();
149 This function is called at a 10 ms interval to fill the replay buffers.
152 void LogFile::timerFired()
154 if (m_replayState
!= PLAYING
) {
159 if (m_file
.bytesAvailable() > TIMESTAMP_SIZE_BYTES
) {
161 time
= m_myTime
.elapsed();
164 This code generates an advancing playback window. All samples that fit the window
165 are replayed. The window is about the size of the timer interval: 10 ms.
167 Description of used variables:
169 time : real-time interval since start of playback (in ms) - now()
170 m_timeOffset : real-time interval since start of playback (in ms) - when timerFired() was previously run
171 m_nextTimeStamp : read log until this log timestamp has been reached (in ms)
172 m_lastPlayed : log referenced timestamp advanced to during previous cycle (in ms)
173 m_playbackSpeed : 0.1 .. 1.0 .. 10 replay speedup factor
177 while (m_nextTimeStamp
< (m_lastPlayed
+ (double)(time
- m_timeOffset
) * m_playbackSpeed
)) {
178 // advance the replay window for the next time period
179 m_lastPlayed
+= ((double)(time
- m_timeOffset
) * m_playbackSpeed
);
183 if (m_file
.bytesAvailable() < (qint64
)sizeof(dataSize
)) {
184 qDebug() << "LogFile replay - end of log file reached";
188 m_file
.read((char *)&dataSize
, sizeof(dataSize
));
190 // check size consistency
191 if (dataSize
< 1 || dataSize
> (1024 * 1024)) {
192 qWarning() << "LogFile replay - corrupted log file! Unlikely packet size:" << dataSize
;
198 if (m_file
.bytesAvailable() < dataSize
) {
199 qDebug() << "LogFile replay - end of log file reached";
203 QByteArray data
= m_file
.read(dataSize
);
205 // make data available
207 m_dataBuffer
.append(data
);
212 // rate-limit slider bar position updates to 10 updates per second
213 if (m_timerTick
% 10 == 0) {
214 emit
playbackPositionChanged(m_nextTimeStamp
);
216 // read next timestamp
217 if (m_file
.bytesAvailable() < (qint64
)sizeof(m_nextTimeStamp
)) {
218 qDebug() << "LogFile replay - end of log file reached";
222 m_previousTimeStamp
= m_nextTimeStamp
;
223 m_file
.read((char *)&m_nextTimeStamp
, sizeof(m_nextTimeStamp
));
225 // some validity checks
226 if ((m_nextTimeStamp
< m_previousTimeStamp
) // logfile goes back in time
227 || ((m_nextTimeStamp
- m_previousTimeStamp
) > 60 * 60 * 1000)) { // gap of more than 60 minutes
228 qWarning() << "LogFile replay - corrupted log file! Unlikely timestamp:" << m_nextTimeStamp
<< "after" << m_previousTimeStamp
;
234 time
= m_myTime
.elapsed(); // number of milliseconds since start of playback
237 qDebug() << "LogFile replay - end of log file reached";
242 bool LogFile::isPlaying() const
244 return m_file
.isOpen() && m_timer
.isActive();
248 * FUNCTION: startReplay()
250 * Starts replaying a newly opened logfile.
251 * Starts a timer: m_timer
253 * This function and the stopReplay() function should only ever be called from the same thread.
254 * This is required for correct control of the timer.
257 bool LogFile::startReplay()
259 // Walk through logfile and create timestamp index
260 // Don't start replay if there was a problem indexing the logfile.
267 if (!m_file
.isOpen() || m_timer
.isActive()) {
270 qDebug() << "LogFile - startReplay";
275 m_previousTimeStamp
= 0;
278 m_dataBuffer
.clear();
281 // read next timestamp
282 if (m_file
.bytesAvailable() < (qint64
)sizeof(m_nextTimeStamp
)) {
283 qWarning() << "LogFile - invalid log file!";
286 m_file
.read((char *)&m_nextTimeStamp
, sizeof(m_nextTimeStamp
));
288 m_timer
.setInterval(10);
290 m_replayState
= PLAYING
;
292 emit
replayStarted();
297 * FUNCTION: stopReplay()
299 * Stops replaying the logfile.
300 * Stops the timer: m_timer
302 * This function and the startReplay() function should only ever be called from the same thread.
303 * This is a requirement to be able to control the timer.
306 bool LogFile::stopReplay()
308 if (!m_file
.isOpen()) {
311 if (m_timer
.isActive()) {
315 qDebug() << "LogFile - stopReplay";
316 m_replayState
= STOPPED
;
318 emit
replayFinished();
323 * FUNCTION: resetReplay()
325 * Stops replaying the logfile.
326 * Stops the timer: m_timer
327 * Resets playback position to the start of the logfile
328 * through the emission of a replayCompleted signal.
331 bool LogFile::resetReplay()
333 if (!m_file
.isOpen()) {
336 if (m_timer
.isActive()) {
340 qDebug() << "LogFile - resetReplay";
341 m_replayState
= STOPPED
;
343 emit
replayCompleted();
348 * SLOT: resumeReplay()
350 * Resumes replay from the given position.
351 * If no position is given, resumes from the last position
354 bool LogFile::resumeReplay(quint32 desiredPosition
)
356 if (m_timer
.isActive()) {
359 qDebug() << "LogFile - resumeReplay";
361 // Clear the playout buffer:
363 m_dataBuffer
.clear();
368 /* Skip through the logfile until we reach the desired position.
369 Looking for the next log timestamp after the desired position
370 has the advantage that it skips over parts of the log
371 where data might be missing.
373 for (int i
= 0; i
< m_timeStamps
.size(); i
++) {
374 if (m_timeStamps
.at(i
) >= desiredPosition
) {
375 int bytesToSkip
= m_timeStampPositions
.at(i
);
376 bool seek_ok
= m_file
.seek(bytesToSkip
);
378 qWarning() << "LogFile resumeReplay - an error occurred while seeking through the logfile.";
380 m_lastPlayed
= m_timeStamps
.at(i
);
384 m_file
.read((char *)&m_nextTimeStamp
, sizeof(m_nextTimeStamp
));
386 // Real-time timestamps don't not need to match the log timestamps.
387 // However the delta between real-time variables "m_timeOffset" and "m_myTime" is important.
388 // This delta determines the number of log entries replayed per cycle.
390 // Set the real-time interval to 0 to start with:
394 m_replayState
= PLAYING
;
398 // Notify UI that playback has resumed
399 emit
replayStarted();
404 * SLOT: pauseReplay()
406 * Pauses replay while storing the current playback position
409 bool LogFile::pauseReplay()
411 if (!m_timer
.isActive()) {
414 qDebug() << "LogFile - pauseReplay";
416 m_replayState
= PAUSED
;
421 * SLOT: pauseReplayAndResetPosition()
423 * Pauses replay and resets the playback position to the start of the logfile
426 bool LogFile::pauseReplayAndResetPosition()
428 if (!m_file
.isOpen() || !m_timer
.isActive()) {
431 qDebug() << "LogFile - pauseReplayAndResetPosition";
433 m_replayState
= STOPPED
;
436 m_lastPlayed
= m_timeStamps
.at(0);
437 m_previousTimeStamp
= 0;
444 * FUNCTION: getReplayState()
446 * Returns the current replay status.
449 ReplayState
LogFile::getReplayState()
451 return m_replayState
;
455 * FUNCTION: buildIndex()
457 * Walk through the opened logfile and stores the first and last position timestamps.
458 * Also builds an index for quickly skipping to a specific position in the logfile.
460 * Returns true when indexing has completed successfully.
461 * Returns false when a problem was encountered.
464 bool LogFile::buildIndex()
468 qint64 readPointer
= 0;
472 qDebug() << "LogFile - buildIndex";
474 // Ensure empty vectors:
475 m_timeStampPositions
.clear();
476 m_timeStamps
.clear();
478 QByteArray arr
= m_file
.readAll();
479 totalSize
= arr
.size();
480 QDataStream
dataStream(&arr
, QIODevice::ReadOnly
);
482 // set the first timestamp
483 if (totalSize
- readPointer
>= TIMESTAMP_SIZE_BYTES
) {
484 bytesRead
= dataStream
.readRawData((char *)&timeStamp
, TIMESTAMP_SIZE_BYTES
);
485 if (bytesRead
!= TIMESTAMP_SIZE_BYTES
) {
486 qWarning() << "LogFile buildIndex - read first timeStamp: readRawData returned unexpected number of bytes:" << bytesRead
<< "at position" << readPointer
<< "\n";
489 m_timeStamps
.append(timeStamp
);
490 m_timeStampPositions
.append(readPointer
);
491 readPointer
+= TIMESTAMP_SIZE_BYTES
;
493 m_beginTimeStamp
= timeStamp
;
494 m_endTimeStamp
= timeStamp
;
500 // Check if there are enough bytes remaining for a correct "dataSize" field
501 if (totalSize
- readPointer
< (qint64
)sizeof(dataSize
)) {
502 qWarning() << "LogFile buildIndex - logfile corrupted! Unexpected end of file";
506 // Read the dataSize field and check for I/O errors
507 bytesRead
= dataStream
.readRawData((char *)&dataSize
, sizeof(dataSize
));
508 if (bytesRead
!= sizeof(dataSize
)) {
509 qWarning() << "LogFile buildIndex - read dataSize: readRawData returned unexpected number of bytes:" << bytesRead
<< "at position" << readPointer
<< "\n";
513 readPointer
+= sizeof(dataSize
);
515 if (dataSize
< 1 || dataSize
> (1024 * 1024)) {
516 qWarning() << "LogFile buildIndex - logfile corrupted! Unlikely packet size: " << dataSize
<< "\n";
520 // Check if there are enough bytes remaining
521 if (totalSize
- readPointer
< dataSize
) {
522 qWarning() << "LogFile buildIndex - logfile corrupted! Unexpected end of file";
526 // skip reading the data (we don't need it at this point)
527 readPointer
+= dataStream
.skipRawData(dataSize
);
529 // read the next timestamp
530 if (totalSize
- readPointer
>= TIMESTAMP_SIZE_BYTES
) {
531 bytesRead
= dataStream
.readRawData((char *)&timeStamp
, TIMESTAMP_SIZE_BYTES
);
532 if (bytesRead
!= TIMESTAMP_SIZE_BYTES
) {
533 qWarning() << "LogFile buildIndex - read timeStamp, readRawData returned unexpected number of bytes:" << bytesRead
<< "at position" << readPointer
<< "\n";
537 // some validity checks
538 if (timeStamp
< m_endTimeStamp
// logfile goes back in time
539 || (timeStamp
- m_endTimeStamp
) > (60 * 60 * 1000)) { // gap of more than 60 minutes)
540 qWarning() << "LogFile buildIndex - logfile corrupted! Unlikely timestamp " << timeStamp
<< " after " << m_endTimeStamp
;
544 m_timeStamps
.append(timeStamp
);
545 m_timeStampPositions
.append(readPointer
);
546 readPointer
+= TIMESTAMP_SIZE_BYTES
;
548 m_endTimeStamp
= timeStamp
;
550 // Break without error (we expect to end at this location when we are at the end of the logfile)
555 emit
timesChanged(m_beginTimeStamp
, m_endTimeStamp
);
557 // reset the read pointer to the start of the file