diff --git a/src/comm/MAVLinkProtocol.cc b/src/comm/MAVLinkProtocol.cc index 4985d90463febdbe8ef7a885f764be514a7c2b87..bb40697cc5a20372f079c64cbc8921ba65a6a3ff 100644 --- a/src/comm/MAVLinkProtocol.cc +++ b/src/comm/MAVLinkProtocol.cc @@ -30,9 +30,13 @@ #include "QGCMAVLink.h" #include "QGCMAVLinkUASFactory.h" #include "QGC.h" +#include "QGCCore.h" Q_DECLARE_METATYPE(mavlink_message_t) +const char* MAVLinkProtocol::_tempLogFileTemplate = "FlightDataXXXXXX"; ///< Template for temporary log file +const char* MAVLinkProtocol::_logFileExtension = "mavlink"; ///< Extension for log files + /** * The default constructor will create a new MAVLink object sending heartbeats at * the MAVLINK_HEARTBEAT_DEFAULT_RATE to all connected links. @@ -43,8 +47,6 @@ MAVLinkProtocol::MAVLinkProtocol() : m_heartbeatsEnabled(true), m_multiplexingEnabled(false), m_authEnabled(false), - m_loggingEnabled(false), - m_logfile(NULL), m_enable_version_check(true), m_paramRetransmissionTimeout(350), m_paramRewriteTimeout(500), @@ -53,9 +55,17 @@ MAVLinkProtocol::MAVLinkProtocol() : m_actionRetransmissionTimeout(100), versionMismatchIgnore(false), systemId(QGC::defaultSystemId), - _should_exit(false) + _should_exit(false), + _logSuspendError(false), + _logSuspendReplay(false), + _protocolStatusMessageConnected(false), + _saveTempFlightDataLogConnected(false) + { qRegisterMetaType("mavlink_message_t"); + + _tempLogFile.setFileTemplate(QString("%1/%2.%3").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).arg(_tempLogFileTemplate).arg(_logFileExtension)); + _tempLogFile.setAutoRemove(false); m_authKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; loadSettings(); @@ -88,18 +98,6 @@ void MAVLinkProtocol::loadSettings() enableVersionCheck(settings.value("VERSION_CHECK_ENABLED", m_enable_version_check).toBool()); enableMultiplexing(settings.value("MULTIPLEXING_ENABLED", m_multiplexingEnabled).toBool()); - // Only set logfile if there is a name present in settings - if (settings.contains("LOGFILE_NAME") && m_logfile == NULL) - { - m_logfile = new QFile(settings.value("LOGFILE_NAME").toString()); - } - else if (m_logfile == NULL) - { - m_logfile = new QFile(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/qgroundcontrol_packetlog.mavlink"); - } - // Enable logging - enableLogging(settings.value("LOGGING_ENABLED", m_loggingEnabled).toBool()); - // Only set system id if it was valid int temp = settings.value("GCS_SYSTEM_ID", systemId).toInt(); if (temp > 0 && temp < 256) @@ -127,17 +125,11 @@ void MAVLinkProtocol::storeSettings() QSettings settings; settings.beginGroup("QGC_MAVLINK_PROTOCOL"); settings.setValue("HEARTBEATS_ENABLED", m_heartbeatsEnabled); - settings.setValue("LOGGING_ENABLED", m_loggingEnabled); settings.setValue("VERSION_CHECK_ENABLED", m_enable_version_check); settings.setValue("MULTIPLEXING_ENABLED", m_multiplexingEnabled); settings.setValue("GCS_SYSTEM_ID", systemId); settings.setValue("GCS_AUTH_KEY", m_authKey); settings.setValue("GCS_AUTH_ENABLED", m_authEnabled); - if (m_logfile) - { - // Logfile exists, store the name - settings.setValue("LOGFILE_NAME", m_logfile->fileName()); - } // Parameter interface settings settings.setValue("PARAMETER_RETRANSMISSION_TIMEOUT", m_paramRetransmissionTimeout); settings.setValue("PARAMETER_REWRITE_TIMEOUT", m_paramRewriteTimeout); @@ -150,16 +142,8 @@ void MAVLinkProtocol::storeSettings() MAVLinkProtocol::~MAVLinkProtocol() { storeSettings(); - if (m_logfile) - { - if (m_logfile->isOpen()) - { - m_logfile->flush(); - m_logfile->close(); - } - delete m_logfile; - m_logfile = NULL; - } + + _closeLogFile(); // Tell the thread to exit _should_exit = true; @@ -192,18 +176,6 @@ void MAVLinkProtocol::run() } } -QString MAVLinkProtocol::getLogfileName() -{ - if (m_logfile) - { - return m_logfile->fileName(); - } - else - { - return QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/qgroundcontrol_packetlog.mavlink"; - } -} - void MAVLinkProtocol::resetMetadataForLink(const LinkInterface *link) { int linkId = link->getId(); @@ -217,17 +189,47 @@ void MAVLinkProtocol::resetMetadataForLink(const LinkInterface *link) void MAVLinkProtocol::linkStatusChanged(bool connected) { LinkInterface* link = qobject_cast(QObject::sender()); + + if (link == NULL) { + Q_ASSERT(false); + return; + } + + if (connected) { + Q_ASSERT(!_connectedLinks.contains(link)); + _connectedLinks.append(link); + + if (_connectedLinks.count() == 1) { + // This is the first link, we need to start logging + _startLogging(); + } + + // Send command to start MAVLink + // XXX hacky but safe + // Start NSH + const char init[] = {0x0d, 0x0d, 0x0d}; + link->writeBytes(init, sizeof(init)); + const char* cmd = "sh /etc/init.d/rc.usb\n"; + link->writeBytes(cmd, strlen(cmd)); + link->writeBytes(init, 4); + } else { + Q_ASSERT(_connectedLinks.contains(link)); + _connectedLinks.removeOne(link); + + if (_connectedLinks.count() == 0) { + // Last link is gone, close out logging + _stopLogging(); + } + } + + // Track the links which are connected to the protocol + QList _connectedLinks; ///< List of all links connected to protocol + + qDebug() << "linkStatusChanged" << connected; + if (link) { if (connected) { - // Send command to start MAVLink - // XXX hacky but safe - // Start NSH - const char init[] = {0x0d, 0x0d, 0x0d}; - link->writeBytes(init, sizeof(init)); - const char* cmd = "sh /etc/init.d/rc.usb\n"; - link->writeBytes(cmd, strlen(cmd)); - link->writeBytes(init, 4); } } } @@ -314,8 +316,10 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) } // Log data - if (m_loggingEnabled && m_logfile) - { + + Q_ASSERT(_logSuspendError || _logSuspendReplay || _tempLogFile.isOpen()); + + if (!_logSuspendError && !_logSuspendReplay) { uint8_t buf[MAVLINK_MAX_PACKET_LEN+sizeof(quint64)]; // Write the uint64 time in microseconds in big endian format before the message. @@ -332,11 +336,12 @@ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) // Now write this timestamp/message pair to the log. QByteArray b((const char*)buf, len); - if(m_logfile->write(b) != len) + if(_tempLogFile.write(b) != len) { // If there's an error logging data, raise an alert and stop logging. - emit protocolStatusMessage(tr("MAVLink Logging failed"), tr("Could not write to file %1, disabling logging.").arg(m_logfile->fileName())); - enableLogging(false); + emit protocolStatusMessage(tr("MAVLink Logging failed"), tr("Could not write to file %1, logging disabled.").arg(_tempLogFile.fileName())); + _stopLogging(); + _logSuspendError = true; } } @@ -633,62 +638,6 @@ void MAVLinkProtocol::setActionRetransmissionTimeout(int ms) } } -void MAVLinkProtocol::enableLogging(bool enabled) -{ - bool changed = false; - if (enabled != m_loggingEnabled) changed = true; - - if (enabled) - { - if (m_logfile && m_logfile->isOpen()) - { - m_logfile->flush(); - m_logfile->close(); - } - - if (m_logfile) - { - if (!m_logfile->open(QIODevice::WriteOnly | QIODevice::Append)) - { - emit protocolStatusMessage(tr("Opening MAVLink logfile for writing failed"), tr("MAVLink cannot log to the file %1, please choose a different file. Stopping logging.").arg(m_logfile->fileName())); - m_loggingEnabled = false; - } - } - else - { - emit protocolStatusMessage(tr("Opening MAVLink logfile for writing failed"), tr("MAVLink cannot start logging, no logfile selected.")); - } - } - else if (!enabled) - { - if (m_logfile) - { - if (m_logfile->isOpen()) - { - m_logfile->flush(); - m_logfile->close(); - } - } - } - m_loggingEnabled = enabled; - if (changed) emit loggingChanged(enabled); -} - -void MAVLinkProtocol::setLogfileName(const QString& filename) -{ - if (!m_logfile) - { - m_logfile = new QFile(filename); - } - else - { - m_logfile->flush(); - m_logfile->close(); - } - m_logfile->setFileName(filename); - enableLogging(m_loggingEnabled); -} - void MAVLinkProtocol::enableVersionCheck(bool enabled) { m_enable_version_check = enabled; @@ -711,3 +660,103 @@ int MAVLinkProtocol::getHeartbeatRate() { return heartbeatRate; } + +/// @brief Closes the log file if it is open +bool MAVLinkProtocol::_closeLogFile(void) +{ + if (_tempLogFile.isOpen()) { + if (_tempLogFile.size() == 0) { + // Don't save zero byte files + _tempLogFile.remove(); + return false; + } else { + _tempLogFile.flush(); + _tempLogFile.close(); + return true; + } + } + + return false; +} + +void MAVLinkProtocol::_startLogging(void) +{ + Q_ASSERT(!_tempLogFile.isOpen()); + + if (!_logSuspendReplay) { + if (!_tempLogFile.open()) { + emit protocolStatusMessage(tr("Opening Flight Data file for writing failed"), + tr("Unable to write to %1. Please choose a different file location.").arg(_tempLogFile.fileName())); + _closeLogFile(); + _logSuspendError = true; + return; + } + + qDebug() << "Temp log" << _tempLogFile.fileName(); + + _logSuspendError = false; + } +} + +void MAVLinkProtocol::_stopLogging(void) +{ + if (_closeLogFile()) { + emit saveTempFlightDataLog(_tempLogFile.fileName()); + } +} + +/// @brief We override this virtual from QObject so that we can track when the +/// protocolStatusMessage and saveTempFlightDataLog signals are connected. +/// Once both are connected we can check for lost log files. +void MAVLinkProtocol::connectNotify(const QMetaMethod& signal) +{ + if (signal == QMetaMethod::fromSignal(&MAVLinkProtocol::protocolStatusMessage)) { + _protocolStatusMessageConnected = true; + if (_saveTempFlightDataLogConnected) { + _checkLostLogFiles(); + } + } else if (signal == QMetaMethod::fromSignal(&MAVLinkProtocol::saveTempFlightDataLog)) { + _saveTempFlightDataLogConnected = true; + if (_protocolStatusMessageConnected) { + _checkLostLogFiles(); + } + } +} + +/// @brief Checks the temp directory for log files which may have been left there. +/// This could happen if QGC crashes without the temp log file being saved. +/// Give the user an option to save these orphaned files. +void MAVLinkProtocol::_checkLostLogFiles(void) +{ + QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + + QString filter(QString("*.%1").arg(_logFileExtension)); + QFileInfoList fileInfoList = tempDir.entryInfoList(QStringList(filter), QDir::Files); + qDebug() << "Orphaned log file count" << fileInfoList.count(); + + foreach(const QFileInfo fileInfo, fileInfoList) { + qDebug() << "Orphaned log file" << fileInfo.filePath(); + if (fileInfo.size() == 0) { + // Delete all zero length files + QFile::remove(fileInfo.filePath()); + continue; + } + + // Give the user a chance to save the orphaned log file + emit protocolStatusMessage(tr("Found unsaved Flight Data"), + tr("This can happen if QGroundControl crashes during Flight Data collection. " + "If you want to save the unsaved Flight Data, select the file you want to save it to. " + "If you do not want to keep the Flight Data, select 'Cancel' on the next dialog.")); + emit saveTempFlightDataLog(fileInfo.filePath()); + } +} + +void MAVLinkProtocol::suspendLogForReplay(bool suspend) +{ + // This must come through a slot so that we handle this on the right thread. This will + // also cause the signal to be queued behind any bytesReady signals which must be flushed + // out of the queue before we change suspend state. + Q_ASSERT(QThread::currentThread() == this); + + _logSuspendReplay = suspend; +} diff --git a/src/comm/MAVLinkProtocol.h b/src/comm/MAVLinkProtocol.h index 05f113231b08f16eef77afb8bfe7e736a0c06bcc..11b8881ee4fae643c5f0ad2dd2bfed83f85b67ed 100644 --- a/src/comm/MAVLinkProtocol.h +++ b/src/comm/MAVLinkProtocol.h @@ -37,6 +37,8 @@ This file is part of the QGROUNDCONTROL project #include #include #include +#include + #include "ProtocolInterface.h" #include "LinkInterface.h" #include "QGCMAVLink.h" @@ -69,10 +71,7 @@ public: bool heartbeatsEnabled() const { return m_heartbeatsEnabled; } - /** @brief Get logging state */ - bool loggingEnabled() const { - return m_loggingEnabled; - } + /** @brief Get protocol version check state */ bool versionCheckEnabled() const { return m_enable_version_check; @@ -93,8 +92,6 @@ public: QString getAuthKey() { return m_authKey; } - /** @brief Get the name of the packet log file */ - QString getLogfileName(); /** @brief Get state of parameter retransmission */ bool paramGuardEnabled() { return m_paramGuardEnabled; @@ -140,7 +137,7 @@ public: * Reset the counters for all metadata for this link. */ virtual void resetMetadataForLink(const LinkInterface *link); - + void run(); public slots: @@ -161,9 +158,6 @@ public slots: /** @brief Enable / disable the heartbeat emission */ void enableHeartbeats(bool enabled); - /** @brief Enable/disable binary packet logging */ - void enableLogging(bool enabled); - /** @brief Enabled/disable packet multiplexing */ void enableMultiplexing(bool enabled); @@ -182,9 +176,6 @@ public slots: /** @brief Set parameter read timeout */ void setActionRetransmissionTimeout(int ms); - /** @brief Set log file name */ - void setLogfileName(const QString& filename); - /** @brief Enable / disable version check */ void enableVersionCheck(bool enabled); @@ -203,16 +194,22 @@ public slots: void loadSettings(); /** @brief Store protocol settings */ void storeSettings(); + + /// @brief Suspend/Restart logging during replay. This must be emitted as a signal + /// and not called directly in order to synchronize with the bytesReady signal + /// which may be ahead of it in the signal queue. + void suspendLogForReplay(bool suspend); + +protected: + // Override from QObject + virtual void connectNotify(const QMetaMethod& signal); -protected: QTimer *heartbeatTimer; ///< Timer to emit heartbeats int heartbeatRate; ///< Heartbeat rate, controls the timer interval bool m_heartbeatsEnabled; ///< Enabled/disable heartbeat emission bool m_multiplexingEnabled; ///< Enable/disable packet multiplexing bool m_authEnabled; ///< Enable authentication token broadcast QString m_authKey; ///< Authentication key - bool m_loggingEnabled; ///< Enable/disable packet logging - QFile* m_logfile; ///< Logfile bool m_enable_version_check; ///< Enable checking of version match of MAV and QGC int m_paramRetransmissionTimeout; ///< Timeout for parameter retransmission int m_paramRewriteTimeout; ///< Timeout for sending re-write request @@ -235,8 +232,6 @@ signals: void messageReceived(LinkInterface* link, mavlink_message_t message); /** @brief Emitted if heartbeat emission mode is changed */ void heartbeatChanged(bool heartbeats); - /** @brief Emitted if logging is started / stopped */ - void loggingChanged(bool enabled); /** @brief Emitted if multiplexing is started / stopped */ void multiplexingChanged(bool enabled); /** @brief Emitted if authentication support is enabled / disabled */ @@ -272,6 +267,27 @@ signals: */ void radioStatusChanged(LinkInterface* link, unsigned rxerrors, unsigned fixed, unsigned rssi, unsigned remrssi, unsigned txbuf, unsigned noise, unsigned remnoise); + + /// @brief Emitted when a temporary log file is ready for saving + void saveTempFlightDataLog(QString tempLogfile); + +private: + bool _closeLogFile(void); + void _startLogging(void); + void _stopLogging(void); + void _checkLostLogFiles(void); + + QList _connectedLinks; ///< List of all links connected to protocol + + bool _logSuspendError; ///< true: Logging suspended due to error + bool _logSuspendReplay; ///< true: Logging suspended due to replay + + QTemporaryFile _tempLogFile; ///< File to log to + static const char* _tempLogFileTemplate; ///< Template for temporary log file + static const char* _logFileExtension; ///< Extension for log files + + bool _protocolStatusMessageConnected; ///< true: protocolStatusMessage signal has been connected + bool _saveTempFlightDataLogConnected; ///< true: saveTempFlightDataLog signal has been connected }; #endif // MAVLINKPROTOCOL_H_ diff --git a/src/ui/MAVLinkSettingsWidget.cc b/src/ui/MAVLinkSettingsWidget.cc index 2e9aaa270ebb11652f950593724e2a0e34988be6..cfd11c5d9d382d8e97df2a79952c82917a8eb4b8 100644 --- a/src/ui/MAVLinkSettingsWidget.cc +++ b/src/ui/MAVLinkSettingsWidget.cc @@ -55,7 +55,6 @@ MAVLinkSettingsWidget::MAVLinkSettingsWidget(MAVLinkProtocol* protocol, QWidget // Initialize state m_ui->heartbeatCheckBox->setChecked(protocol->heartbeatsEnabled()); - m_ui->loggingCheckBox->setChecked(protocol->loggingEnabled()); m_ui->versionCheckBox->setChecked(protocol->versionCheckEnabled()); m_ui->multiplexingCheckBox->setChecked(protocol->multiplexingEnabled()); m_ui->systemIdSpinBox->setValue(protocol->getSystemId()); @@ -71,14 +70,9 @@ MAVLinkSettingsWidget::MAVLinkSettingsWidget(MAVLinkProtocol* protocol, QWidget // Heartbeat connect(protocol, SIGNAL(heartbeatChanged(bool)), m_ui->heartbeatCheckBox, SLOT(setChecked(bool))); connect(m_ui->heartbeatCheckBox, SIGNAL(toggled(bool)), protocol, SLOT(enableHeartbeats(bool))); - // Logging - connect(protocol, SIGNAL(loggingChanged(bool)), m_ui->loggingCheckBox, SLOT(setChecked(bool))); - connect(m_ui->loggingCheckBox, SIGNAL(toggled(bool)), protocol, SLOT(enableLogging(bool))); // Version check connect(protocol, SIGNAL(versionCheckChanged(bool)), m_ui->versionCheckBox, SLOT(setChecked(bool))); connect(m_ui->versionCheckBox, SIGNAL(toggled(bool)), protocol, SLOT(enableVersionCheck(bool))); - // Logfile - connect(m_ui->logFileButton, SIGNAL(clicked()), this, SLOT(chooseLogfileName())); // System ID connect(protocol, SIGNAL(systemIdChanged(int)), m_ui->systemIdSpinBox, SLOT(setValue(int))); connect(m_ui->systemIdSpinBox, SIGNAL(valueChanged(int)), protocol, SLOT(setSystemId(int))); @@ -110,16 +104,10 @@ MAVLinkSettingsWidget::MAVLinkSettingsWidget(MAVLinkProtocol* protocol, QWidget // Update values m_ui->versionLabel->setText(tr("MAVLINK_VERSION: %1").arg(protocol->getVersion())); - updateLogfileName(protocol->getLogfileName()); // Connect visibility updates connect(protocol, SIGNAL(versionCheckChanged(bool)), m_ui->versionLabel, SLOT(setVisible(bool))); m_ui->versionLabel->setVisible(protocol->versionCheckEnabled()); - // Logging visibility - connect(protocol, SIGNAL(loggingChanged(bool)), m_ui->logFileLabel, SLOT(setVisible(bool))); - m_ui->logFileLabel->setVisible(protocol->loggingEnabled()); - connect(protocol, SIGNAL(loggingChanged(bool)), m_ui->logFileButton, SLOT(setVisible(bool))); - m_ui->logFileButton->setVisible(protocol->loggingEnabled()); // // Multiplexing visibility // connect(protocol, SIGNAL(multiplexingChanged(bool)), m_ui->multiplexingFilterCheckBox, SLOT(setVisible(bool))); // m_ui->multiplexingFilterCheckBox->setVisible(protocol->multiplexingEnabled()); @@ -146,39 +134,6 @@ MAVLinkSettingsWidget::MAVLinkSettingsWidget(MAVLinkProtocol* protocol, QWidget m_ui->multiplexingFilterLineEdit->setVisible(false); } -void MAVLinkSettingsWidget::updateLogfileName(const QString& fileName) -{ - QFileInfo file(fileName); - m_ui->logFileLabel->setText(file.fileName()); -} - -void MAVLinkSettingsWidget::chooseLogfileName() -{ - QString fileName = QFileDialog::getSaveFileName(this, tr("Specify MAVLink log file name"), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation), tr("MAVLink Logfile (*.mavlink);;")); - - if (!fileName.endsWith(".mavlink")) - { - fileName.append(".mavlink"); - } - - QFileInfo file(fileName); - if (file.exists() && !file.isWritable()) - { - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText(tr("The selected logfile is not writable")); - msgBox.setInformativeText(tr("Please make sure that the file %1 is writable or select a different file").arg(fileName)); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); - } - else - { - updateLogfileName(fileName); - protocol->setLogfileName(fileName); - } -} - void MAVLinkSettingsWidget::enableDroneOS(bool enable) { // Enable multiplexing diff --git a/src/ui/MAVLinkSettingsWidget.h b/src/ui/MAVLinkSettingsWidget.h index 743daaf243561eaac3d6f63e7692e25fa87709d2..a5d4a1358e1815e01c6c854e5c3931cf18fa244d 100644 --- a/src/ui/MAVLinkSettingsWidget.h +++ b/src/ui/MAVLinkSettingsWidget.h @@ -18,10 +18,6 @@ public: ~MAVLinkSettingsWidget(); public slots: - /** @brief Update the log file name display */ - void updateLogfileName(const QString& fileName); - /** @brief Start the file select dialog for the log file */ - void chooseLogfileName(); /** @brief Enable DroneOS forwarding */ void enableDroneOS(bool enable); diff --git a/src/ui/MAVLinkSettingsWidget.ui b/src/ui/MAVLinkSettingsWidget.ui index 09e73d1fc8903e2755320b1708e2d3c26998e088..fa7c3c6fd254eb82adb0ae9584993ed23ab46ee3 100644 --- a/src/ui/MAVLinkSettingsWidget.ui +++ b/src/ui/MAVLinkSettingsWidget.ui @@ -7,35 +7,18 @@ 0 0 431 - 530 + 442 Form - - - - - Emit heartbeat - - - - - - - Log all MAVLink packets - - - - - + + + Qt::Horizontal - - QSizePolicy::MinimumExpanding - 8 @@ -44,15 +27,8 @@ - - - - Only accept MAVs with same protocol version - - - - - + + Qt::Horizontal @@ -64,72 +40,28 @@ - - - - MAVLINK_VERSION: - - - - - - - Logfile name - - - - - - - Select.. - - - - - + + - Set the groundstation number + Time in milliseconds after which a not acknowledged write request is sent again. - Set the groundstation number + Time in milliseconds after which a not acknowledged write request is sent again. + + + ms - 1 + 50 - 255 - - - - - - - The system ID is the number the MAV associates with this computer - - - The system ID is the number the MAV associates with this computer + 60000 - - Groundstation MAVLink System ID + + 50 - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 8 - 0 - - - - @@ -137,28 +69,23 @@ - - - - Filter multiplexed packets: Only forward selected IDs + + + + ms - - - - - - Enable Multiplexing: Forward packets to all other links + + 20 - - - - - - Enable retransmission of parameter read/write requests + + 1000 + + + 10 - + Qt::Horizontal @@ -171,34 +98,7 @@ - - - - Qt::Horizontal - - - - 8 - 0 - - - - - - - - Read request retransmission timeout - - - - - - - Write request retransmission timeout - - - - + Time in milliseconds after which a not acknowledged read request is sent again. @@ -223,37 +123,41 @@ - - - - Time in milliseconds after which a not acknowledged write request is sent again. - - - Time in milliseconds after which a not acknowledged write request is sent again. - - - ms - - - 50 + + + + + 0 + 0 + - - 60000 + + 0 - - 50 + + Qt::Horizontal - - - - Enable retransmission of actions / commands + + + + true + + + mavlink.droneos.com:14555 + + + + + localhost:14555 + + - - + + Qt::Horizontal @@ -265,57 +169,106 @@ - - + + - Action request retransmission timeout + Write request retransmission timeout - - - - ms + + + + Read request retransmission timeout - - 20 + + + + + + Emit heartbeat - - 1000 + + + + + + Enable Multiplexing: Forward packets to all other links - - 10 + + + + + + Action request retransmission timeout - - - + + + + Qt::Horizontal + + - 0 + 8 0 - - 0 + + + + + + Set the groundstation number - - Qt::Horizontal + + Set the groundstation number + + + 1 + + + 255 - + Forward MAVLink packets of all links to http://droneos.com - - + + + + Enable retransmission of actions / commands + + + + + + + Filter multiplexed packets: Only forward selected IDs + + + + + + + Only accept MAVs with same protocol version + + + + + Qt::Horizontal + + QSizePolicy::MinimumExpanding + 8 @@ -324,7 +277,7 @@ - + Enter your DroneOS API Token/Key @@ -334,21 +287,31 @@ - - - - true + + + + Enable retransmission of parameter read/write requests + + + + + + + MAVLINK_VERSION: + + + + + + + The system ID is the number the MAV associates with this computer + + + The system ID is the number the MAV associates with this computer + + + Groundstation MAVLink System ID - - - mavlink.droneos.com:14555 - - - - - localhost:14555 - - diff --git a/src/ui/MainWindow.cc b/src/ui/MainWindow.cc index c6cdc06442b158bb19da4e5e2aace8f94df8a851..c4123047d9172fea70070b127c7e2223189d5ae2 100644 --- a/src/ui/MainWindow.cc +++ b/src/ui/MainWindow.cc @@ -70,6 +70,7 @@ This file is part of the QGROUNDCONTROL project #include "menuactionhelper.h" #include "QGCUASFileViewMulti.h" #include +#include "QGCCore.h" #ifdef QGC_OSG_ENABLED #include "Q3DWidgetFactory.h" @@ -80,41 +81,38 @@ This file is part of the QGROUNDCONTROL project #include "LogCompressor.h" +static MainWindow* _instance = NULL; ///< @brief MainWindow singleton + // Set up some constants const QString MainWindow::defaultDarkStyle = ":files/styles/style-dark.css"; const QString MainWindow::defaultLightStyle = ":files/styles/style-light.css"; -MainWindow* MainWindow::instance_mode(QSplashScreen* screen, enum MainWindow::CUSTOM_MODE mode) +MainWindow* MainWindow::_create(QSplashScreen* splashScreen, enum MainWindow::CUSTOM_MODE mode) { - static MainWindow* _instance = 0; - if (_instance == 0) - { - _instance = new MainWindow(); - _instance->setCustomMode(mode); - if (screen) - { - connect(_instance, SIGNAL(initStatusChanged(QString,int,QColor)), - screen, SLOT(showMessage(QString,int,QColor))); - } - _instance->init(); - } + Q_ASSERT(_instance == NULL); + Q_ASSERT(splashScreen); + + new MainWindow(splashScreen, mode); + + // _instance is set in constructor + Q_ASSERT(_instance); + return _instance; } -MainWindow* MainWindow::instance(QSplashScreen* screen) +MainWindow* MainWindow::instance(void) { - return instance_mode(screen, CUSTOM_MODE_UNCHANGED); + // QGCAppication should have already called _create. Singleton is only created by call to _create + // not here. + Q_ASSERT(_instance); + + return _instance; } -/** -* Create new mainwindow. The constructor instantiates all parts of the user -* interface. It does NOT show the mainwindow. To display it, call the show() -* method. -* -* @see QMainWindow::show() -**/ -MainWindow::MainWindow(QWidget *parent): - QMainWindow(parent), +/// @brief Private constructor for MainWindow. MainWindow singleton is only ever created +/// by MainWindow::_create method. Hence no other code should have access to +/// constructor. +MainWindow::MainWindow(QSplashScreen* splashScreen, enum MainWindow::CUSTOM_MODE mode) : currentView(VIEW_FLIGHT), currentStyle(QGC_MAINWINDOW_STYLE_DARK), aboutToCloseFlag(false), @@ -124,19 +122,25 @@ MainWindow::MainWindow(QWidget *parent): autoReconnect(false), simulationLink(NULL), lowPowerMode(false), - customMode(CUSTOM_MODE_NONE), - menuActionHelper(new MenuActionHelper()) + customMode(mode), + menuActionHelper(new MenuActionHelper()), + _splashScreen(splashScreen) { + Q_ASSERT(splashScreen); + + Q_ASSERT(_instance == NULL); + _instance = this; + + connect(this, &MainWindow::initStatusChanged, splashScreen, &QSplashScreen::showMessage); + this->setAttribute(Qt::WA_DeleteOnClose); connect(menuActionHelper, SIGNAL(needToShowDockWidget(QString,bool)),SLOT(showDockWidget(QString,bool))); - //TODO: move protocol outside UI - connect(mavlink, SIGNAL(protocolStatusMessage(QString,QString)), this, SLOT(showCriticalMessage(QString,QString)), Qt::QueuedConnection); + + connect(mavlink, &MAVLinkProtocol::protocolStatusMessage, this, &MainWindow::showCriticalMessage); + connect(mavlink, &MAVLinkProtocol::saveTempFlightDataLog, this, &MainWindow::_saveTempFlightDataLog); + loadSettings(); -} - -void MainWindow::init() -{ - + emit initStatusChanged(tr("Loading style"), Qt::AlignLeft | Qt::AlignBottom, QColor(62, 93, 141)); qApp->setStyle("plastique"); loadStyle(currentStyle); @@ -261,11 +265,15 @@ void MainWindow::init() // Connect link if (autoReconnect) { + LinkManager* linkMgr = LinkManager::instance(); + Q_ASSERT(linkMgr); + SerialLink* link = new SerialLink(); + // Add to registry - LinkManager::instance()->add(link); - LinkManager::instance()->addProtocol(link, mavlink); - link->connect(); + linkMgr->add(link); + linkMgr->addProtocol(link, mavlink); + linkMgr->connectLink(link); } // Set low power mode @@ -1122,24 +1130,16 @@ void MainWindow::showStatusMessage(const QString& status) void MainWindow::showCriticalMessage(const QString& title, const QString& message) { - QMessageBox msgBox(this); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText(title); - msgBox.setInformativeText(message); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); + _hideSplashScreen(); + qDebug() << "Critical" << title << message; + QMessageBox::critical(this, title, message); } void MainWindow::showInfoMessage(const QString& title, const QString& message) { - QMessageBox msgBox(this); - msgBox.setIcon(QMessageBox::Information); - msgBox.setText(title); - msgBox.setInformativeText(message); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); + _hideSplashScreen(); + qDebug() << "Information" << title << message; + QMessageBox::information(this, title, message); } /** @@ -1276,52 +1276,37 @@ void MainWindow::connectCommonActions() connect(ui.actionSimulate, SIGNAL(triggered(bool)), this, SLOT(simulateLink(bool))); } -void MainWindow::showHelp() +void MainWindow::_openUrl(const QString& url, const QString& errorMessage) { - if(!QDesktopServices::openUrl(QUrl("http://qgroundcontrol.org/users/start"))) - { - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText("Could not open help in browser"); - msgBox.setInformativeText("To get to the online help, please open http://qgroundcontrol.org/user_guide in a browser."); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); + if(!QDesktopServices::openUrl(QUrl(url))) { + QMessageBox::critical(this, + tr("Could not open information in browser"), + errorMessage); } } +void MainWindow::showHelp() +{ + _openUrl("http://qgroundcontrol.org/users/start", + tr("To get to the online help, please open http://qgroundcontrol.org/user_guide in a browser.")); +} + void MainWindow::showCredits() { - if(!QDesktopServices::openUrl(QUrl("http://qgroundcontrol.org/credits"))) - { - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText("Could not open credits in browser"); - msgBox.setInformativeText("To get to the online help, please open http://qgroundcontrol.org/credits in a browser."); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); - } + _openUrl("http://qgroundcontrol.org/credits", + tr("To get to the credits, please open http://qgroundcontrol.org/credits in a browser.")); } void MainWindow::showRoadMap() { - if(!QDesktopServices::openUrl(QUrl("http://qgroundcontrol.org/dev/roadmap"))) - { - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText("Could not open roadmap in browser"); - msgBox.setInformativeText("To get to the online help, please open http://qgroundcontrol.org/roadmap in a browser."); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); - } + _openUrl("http://qgroundcontrol.org/dev/roadmap", + tr("To get to the online help, please open http://qgroundcontrol.org/roadmap in a browser.")); } void MainWindow::showSettings() { - QGCSettingsWidget* settings = new QGCSettingsWidget(joystick, this); - settings->show(); + SettingsDialog settings(joystick, this); + settings.exec(); } LinkInterface* MainWindow::addLink() @@ -1412,9 +1397,16 @@ void MainWindow::addLink(LinkInterface *link) } void MainWindow::simulateLink(bool simulate) { - if (!simulationLink) - simulationLink = new MAVLinkSimulationLink(":/demo-log.txt"); - simulationLink->connectLink(simulate); + if (simulate) { + if (!simulationLink) { + simulationLink = new MAVLinkSimulationLink(":/demo-log.txt"); + Q_CHECK_PTR(simulationLink); + } + LinkManager::instance()->connectLink(simulationLink); + } else { + Q_ASSERT(simulationLink); + LinkManager::instance()->disconnectLink(simulationLink); + } } void MainWindow::commsWidgetDestroyed(QObject *obj) @@ -1700,6 +1692,9 @@ void MainWindow::handleMisconfiguration(UASInterface* uas) return; } } + + _hideSplashScreen(); + // Ask user if he wants to handle this now QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Information); @@ -1826,6 +1821,31 @@ bool MainWindow::dockWidgetTitleBarsEnabled() const return menuActionHelper->dockWidgetTitleBarsEnabled(); } +void MainWindow::_saveTempFlightDataLog(QString tempLogfile) +{ + if (qgcApp()->promptFlightDataSave()) { + _hideSplashScreen(); + QString saveFilename = QFileDialog::getSaveFileName(this, + tr("Select file to save Flight Data Log"), + qgcApp()->mavlinkLogFilesLocation(), + tr("Flight Data Log (*.mavlink)")); + if (!saveFilename.isEmpty()) { + QFile::copy(tempLogfile, saveFilename); + } + } + QFile::remove(tempLogfile); +} + +/// @brief Hides the spash screen if it is currently being shown +void MainWindow::_hideSplashScreen(void) +{ + if (_splashScreen) { + _splashScreen->hide(); + _splashScreen = NULL; + } +} + + #ifdef QGC_MOUSE_ENABLED_LINUX bool MainWindow::x11Event(XEvent *event) { diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index bdcaaf834a2b99a64a7da1bf5e79269abe111272..67113e67a5d07d40dd3a6ea5037b24a8b0bf4916 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -100,31 +100,18 @@ public: CUSTOM_MODE_WIFI }; - /** - * A static function for obtaining the sole instance of the MainWindow. The screen - * argument is only important on the FIRST call to this function. The provided splash - * screen is updated with some status messages that are emitted during init(). This - * function cannot be used within the MainWindow constructor! - */ - static MainWindow* instance(QSplashScreen* screen = 0); - static MainWindow* instance_mode(QSplashScreen* screen = 0, enum MainWindow::CUSTOM_MODE mode = MainWindow::CUSTOM_MODE_NONE); + /// @brief Returns the MainWindow singleton. Will not create the MainWindow if it has not already + /// been created. + static MainWindow* instance(void); + + /// @brief Creates the MainWindow singleton. Should only be called once by QGCApplication. + static MainWindow* _create(QSplashScreen* splashScreen, enum MainWindow::CUSTOM_MODE mode); + + /// @brief Called to indicate that splash screen is no longer being displayed. + void splashScreenFinished(void) { _splashScreen = NULL; } - /** - * Initializes the MainWindow. Some variables are initialized and the widget is hidden. - * Initialization of the MainWindow class really occurs in init(), which loads the UI - * and does everything important. The constructor is split in two like this so that - * the instance() is available for all classes. - */ - MainWindow(QWidget *parent = NULL); ~MainWindow(); - /** - * This function actually performs the non-trivial initialization of the MainWindow - * class. This is separate from the constructor because instance() won't work within - * code executed in the MainWindow constructor. - */ - void init(); - enum QGC_MAINWINDOW_STYLE { QGC_MAINWINDOW_STYLE_DARK, @@ -272,7 +259,7 @@ public slots: void configureWindowName(); void commsWidgetDestroyed(QObject *obj); - + protected slots: void showDockWidget(const QString &name, bool show); /** @@ -475,8 +462,18 @@ protected: QGCFlightGearLink* fgLink; QTimer windowNameUpdateTimer; CUSTOM_MODE customMode; + +private slots: + /// @brief Save the specified Flight Data Log + void _saveTempFlightDataLog(QString tempLogfile); private: + /// Constructor is private since all creation should be through MainWindow::instance. + MainWindow(QSplashScreen* splashScreen, enum MainWindow::CUSTOM_MODE mode); + + void _hideSplashScreen(void); + void _openUrl(const QString& url, const QString& errorMessage); + QList commsWidgetList; QMap customWidgetNameToFilenameMap; MenuActionHelper *menuActionHelper; @@ -489,6 +486,8 @@ private: QString getWindowStateKey(); QString getWindowGeometryKey(); + + QSplashScreen* _splashScreen; ///< Splash screen, NULL is splash screen not currently being shown friend class MenuActionHelper; //For VIEW_SECTIONS }; diff --git a/src/ui/QGCMAVLinkLogPlayer.cc b/src/ui/QGCMAVLinkLogPlayer.cc index 7826d0e8513370811d0ca754d93da295b17a5e78..8d8e298300b4f0f7128c05bd1b2c4df6477059f1 100644 --- a/src/ui/QGCMAVLinkLogPlayer.cc +++ b/src/ui/QGCMAVLinkLogPlayer.cc @@ -8,6 +8,8 @@ #include "QGCMAVLinkLogPlayer.h" #include "QGC.h" #include "ui_QGCMAVLinkLogPlayer.h" +#include "QGCCore.h" +#include "LinkManager.h" QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *parent) : QWidget(parent), @@ -22,9 +24,10 @@ QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *pare binaryBaudRate(defaultBinaryBaudRate), isPlaying(false), currPacketCount(0), - lastLogDirectory(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)), ui(new Ui::QGCMAVLinkLogPlayer) { + Q_ASSERT(mavlink); + ui->setupUi(this); ui->horizontalLayout->setAlignment(Qt::AlignTop); @@ -32,14 +35,18 @@ QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *pare connect(this, SIGNAL(bytesReady(LinkInterface*,QByteArray)), mavlink, SLOT(receiveBytes(LinkInterface*,QByteArray))); // Setup timer - connect(&loopTimer, SIGNAL(timeout()), this, SLOT(logLoop())); + connect(&loopTimer, &QTimer::timeout, this, &QGCMAVLinkLogPlayer::logLoop); // Setup buttons - connect(ui->selectFileButton, SIGNAL(clicked()), this, SLOT(selectLogFile())); - connect(ui->playButton, SIGNAL(clicked()), this, SLOT(playPauseToggle())); - connect(ui->speedSlider, SIGNAL(valueChanged(int)), this, SLOT(setAccelerationFactorInt(int))); - connect(ui->positionSlider, SIGNAL(valueChanged(int)), this, SLOT(jumpToSliderVal(int))); - connect(ui->positionSlider, SIGNAL(sliderPressed()), this, SLOT(pause())); + 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); + + // We use this to queue the signal over to mavlink. This way it will be behind any remaining + // bytesReady signals in the queue. + connect(this, &QGCMAVLinkLogPlayer::suspendLogForReplay, mavlink, &MAVLinkProtocol::suspendLogForReplay); setAccelerationFactorInt(49); ui->speedSlider->setValue(49); @@ -53,29 +60,14 @@ QGCMAVLinkLogPlayer::QGCMAVLinkLogPlayer(MAVLinkProtocol* mavlink, QWidget *pare 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, SIGNAL(logFileEndReached()), &loopTimer, SLOT(stop())); - - loadSettings(); + connect(this, &QGCMAVLinkLogPlayer::logFileEndReached, &loopTimer, &QTimer::stop); } QGCMAVLinkLogPlayer::~QGCMAVLinkLogPlayer() { - storeSettings(); delete ui; } -void QGCMAVLinkLogPlayer::playPause(bool enabled) -{ - if (enabled) - { - play(); - } - else - { - pause(); - } -} - void QGCMAVLinkLogPlayer::playPauseToggle() { if (isPlaying) @@ -90,55 +82,49 @@ void QGCMAVLinkLogPlayer::playPauseToggle() void QGCMAVLinkLogPlayer::play() { - if (logFile.isOpen()) + Q_ASSERT(logFile.isOpen()); + + LinkManager::instance()->setConnectionsSuspended(tr("Connect not allowed during Flight Data replay.")); + emit suspendLogForReplay(true); + + // 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()) { - // 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(); - } + 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; + // 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(":files/images/actions/media-playback-pause.svg")); + // Start timer + if (mavlinkLogFormat) + { + loopTimer.start(1); } else { - ui->playButton->setChecked(false); - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Information); - msgBox.setText(tr("No logfile selected")); - msgBox.setInformativeText(tr("Please select first a MAVLink log file before playing it.")); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); + // 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(":files/images/actions/media-playback-pause.svg")); } void QGCMAVLinkLogPlayer::pause() { + LinkManager::instance()->setConnectionsAllowed(); + emit suspendLogForReplay(false); + loopTimer.stop(); isPlaying = false; ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-start.svg")); @@ -258,51 +244,33 @@ void QGCMAVLinkLogPlayer::updatePositionSliderUi(float percent) ui->positionSlider->blockSignals(false); } -void QGCMAVLinkLogPlayer::loadSettings() +/// @brief Displays a file dialog to allow the user to select a log file to play back. +void QGCMAVLinkLogPlayer::_selectLogFileForPlayback(void) { - QSettings settings; - settings.beginGroup("QGC_MAVLINKLOGPLAYER"); - lastLogDirectory = settings.value("LAST_LOG_DIRECTORY", lastLogDirectory).toString(); - settings.endGroup(); -} - -void QGCMAVLinkLogPlayer::storeSettings() -{ - QSettings settings; - settings.beginGroup("QGC_MAVLINKLOGPLAYER"); - settings.setValue("LAST_LOG_DIRECTORY", lastLogDirectory); - settings.endGroup(); - settings.sync(); -} - -/** - * @brief Select a log file - * @param startDirectory Directory where the file dialog will be opened - * @return filename of the logFile - */ -bool QGCMAVLinkLogPlayer::selectLogFile() -{ - // Prompt the user for a new file using the last directory they searched. - return selectLogFile(lastLogDirectory); -} - -/** - * @brief Select a log file - * @param startDirectory Directory where the file dialog will be opened - * @return filename of the logFile - */ -bool QGCMAVLinkLogPlayer::selectLogFile(const QString startDirectory) -{ - QString fileName = QFileDialog::getOpenFileName(this, tr("Specify MAVLink log file name to replay"), startDirectory, tr("MAVLink or Binary Logfile (*.mavlink *.bin *.log)")); - - if (fileName == "") - { - return false; + // Disallow replay when any links are connected + + bool foundConnection = false; + LinkManager* linkMgr = LinkManager::instance(); + QList links = linkMgr->getLinks(); + foreach(LinkInterface* link, links) { + if (link->isConnected()) { + foundConnection = true; + break; + } } - else - { - lastLogDirectory = fileName; - return loadLogFile(fileName); + + if (foundConnection) { + MainWindow::instance()->showInfoMessage(tr("Log Replay"), tr("You must close all connections prior to replaying a log.")); + return; + } + + QString logFile = QFileDialog::getOpenFileName(this, + tr("Specify MAVLink log file name to replay"), + qgcApp()->mavlinkLogFilesLocation(), + tr("MAVLink or Binary Logfile (*.mavlink *.bin *.log)")); + + if (!logFile.isEmpty()) { + loadLogFile(logFile); } } @@ -341,157 +309,125 @@ void QGCMAVLinkLogPlayer::setAccelerationFactorInt(int factor) bool QGCMAVLinkLogPlayer::loadLogFile(const QString& file) { - // 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); - - // Disable logging while replaying a log file. - if (mavlink->loggingEnabled()) - { - mavlink->enableLogging(false); - MainWindow::instance()->showInfoMessage(tr("MAVLink Logging Stopped during Replay"), tr("MAVLink logging has been stopped during the log replay. To re-enable logging, use the link properties in the communication menu.")); - } - // Make sure to stop the logging process and reset everything. reset(); - - // And that the old file is closed nicely. - if (logFile.isOpen()) - { - logFile.close(); - } + logFile.close(); // Now load the new file. logFile.setFileName(file); - if (!logFile.open(QFile::ReadOnly)) - { - MainWindow::instance()->showCriticalMessage(tr("The selected logfile is unreadable"), tr("Please make sure that the file %1 is readable or select a different file").arg(file)); - logFile.setFileName(""); + if (!logFile.open(QFile::ReadOnly)) { + MainWindow::instance()->showCriticalMessage(tr("The selected file is unreadable"), tr("Please make sure that the file %1 is readable or select a different file").arg(file)); + _playbackError(); return false; } - else - { - QFileInfo logFileInfo(file); - logFile.reset(); - ui->logFileNameLabel->setText(tr("Logfile: %1").arg(logFileInfo.fileName())); - - // If there's an existing MAVLinkSimulationLink() being used for an old file, - // we replace it. - if (logLink) - { - logLink->disconnect(); - LinkManager::instance()->removeLink(logLink); - delete logLink; - } - logLink = new MAVLinkSimulationLink(""); + + 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()->disconnectLink(logLink); + LinkManager::instance()->removeLink(logLink); + logLink->deleteLater(); + } + logLink = new MAVLinkSimulationLink(""); - // 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; - } + // Select if binary or MAVLink log format is used + mavlinkLogFormat = file.endsWith(".mavlink"); - if (endtime == starttime) { - MainWindow::instance()->showCriticalMessage(tr("The selected logfile cannot be processed"), tr("No valid timestamps were found at the end of the logfile.").arg(file)); - logFile.setFileName(""); - ui->logFileNameLabel->setText(tr("No logfile selected")); - return false; - } + 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; + } - // Remember the start and end time so we can move around this logfile with the slider. - logEndTime = endtime; - logStartTime = starttime; - logCurrentTime = logStartTime; + if (endtime == starttime) { + MainWindow::instance()->showCriticalMessage(tr("The selected file is corrupt"), tr("No valid timestamps were found at the end of the file.").arg(file)); + _playbackError(); + return false; + } - // Reset our log file so when we go to read it for the first time, we start at the beginning. - logFile.reset(); + // Remember the start and end time so we can move around this logfile with the slider. + logEndTime = endtime; + logStartTime = starttime; + logCurrentTime = logStartTime; - // 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; + // Reset our log file so when we go to read it for the first time, we start at the beginning. + logFile.reset(); - // 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 + // 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) { - // 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)) { - 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; - } + // 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)); } - // Check if a serial link is connected - - bool linkWarning = false; - foreach (LinkInterface* link, LinkManager::instance()->getLinks()) - { - SerialLink* s = dynamic_cast(link); - - if (s && s->isConnected()) - linkWarning = true; - } + int seconds = logFileInfo.size() / (binaryBaudRate / 10); + int minutes = seconds / 60; + int hours = minutes / 60; + seconds -= 60*minutes; + minutes -= 60*hours; - if (linkWarning) - MainWindow::instance()->showInfoMessage(tr("Active MAVLink links found"), tr("Currently other links are connected. It is recommended to disconnect any active link before replaying a log.")); + 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)); + } - play(); + // 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 true; } quint64 QGCMAVLinkLogPlayer::parseTimestamp(const QByteArray &data) @@ -593,17 +529,8 @@ void QGCMAVLinkLogPlayer::logLoop() emit bytesReady(logLink, message); // If we've reached the end of the of the file, make sure we handle that well - if (logFile.atEnd()) - { - // For some reason calling pause() here doesn't work, so we update the UI manually here. - isPlaying = false; - ui->playButton->setIcon(QIcon(":files/images/actions/media-playback-start.svg")); - ui->playButton->setChecked(false); - ui->selectFileButton->setEnabled(true); - - // 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); - emit logFileEndReached(); + if (logFile.atEnd()) { + _finishPlayback(); return; } @@ -633,12 +560,7 @@ void QGCMAVLinkLogPlayer::logLoop() // Check if reached end of file before reading next timestamp if (chunk.length() < len || logFile.atEnd()) { - // Reached end of file - reset(); - - QString status = tr("Reached end of binary log file."); - ui->logStatsLabel->setText(status); - MainWindow::instance()->showStatusMessage(status); + _finishPlayback(); return; } } @@ -707,3 +629,40 @@ void QGCMAVLinkLogPlayer::paintEvent(QPaintEvent *) QPainter p(this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } + +/// @brief Called when playback is complete +void QGCMAVLinkLogPlayer::_finishPlayback(void) +{ + pause(); + + QString status = tr("Flight Data replay complete"); + ui->logStatsLabel->setText(status); + MainWindow::instance()->showStatusMessage(status); + + // 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); + + emit logFileEndReached(); + + emit suspendLogForReplay(false); + LinkManager::instance()->setConnectionsAllowed(); +} + +/// @brief Called when an error occurs during playback to reset playback system state. +void QGCMAVLinkLogPlayer::_playbackError(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); +} diff --git a/src/ui/QGCMAVLinkLogPlayer.h b/src/ui/QGCMAVLinkLogPlayer.h index 05887b7b2cf227f7105c8fb46f24818b3e37a533..0b3807b25b57b07f96f9eb1b458523943d614f76 100644 --- a/src/ui/QGCMAVLinkLogPlayer.h +++ b/src/ui/QGCMAVLinkLogPlayer.h @@ -37,29 +37,15 @@ public: return logFile.isOpen(); } - /** - * @brief Set the last log file name - * @param filename - */ - void setLastLogFile(const QString& filename) { - lastLogDirectory = filename; - } - public slots: /** @brief Toggle between play and pause */ void playPauseToggle(); - /** @brief Play / pause the log */ - void playPause(bool play); /** @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 Select logfile */ - bool selectLogFile(const QString startDirectory); - /** @brief Select logfile */ - bool selectLogFile(); /** @brief Load log file */ bool loadLogFile(const QString& file); /** @brief Jump to a position in the logfile */ @@ -73,6 +59,9 @@ signals: /** @brief Send ready bytes */ void bytesReady(LinkInterface* link, const QByteArray& bytes); void logFileEndReached(); + + /// @brief Connected to the MAVLinkProtocol::suspendLogForReplay + void suspendLogForReplay(bool suspend); 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. @@ -92,11 +81,10 @@ protected: unsigned int currPacketCount; static const int packetLen = MAVLINK_MAX_PACKET_LEN; static const int timeLen = sizeof(quint64); - QString lastLogDirectory; void changeEvent(QEvent *e); - - void loadSettings(); - void storeSettings(); + +private slots: + void _selectLogFileForPlayback(void); private: Ui::QGCMAVLinkLogPlayer *ui; @@ -130,6 +118,9 @@ private: * @return True if the new file position was successfully jumped to, false otherwise */ bool jumpToPlaybackLocation(float percentage); + + void _finishPlayback(void); + void _playbackError(void); }; #endif // QGCMAVLINKLOGPLAYER_H diff --git a/src/ui/QGCMAVLinkLogPlayer.ui b/src/ui/QGCMAVLinkLogPlayer.ui index a94f120fab1ad21ac8f3ca22855164b75e57a568..a173fd8936f3c4b6ffcad71f7c141c0824b07ffe 100644 --- a/src/ui/QGCMAVLinkLogPlayer.ui +++ b/src/ui/QGCMAVLinkLogPlayer.ui @@ -36,13 +36,13 @@ - Start to replay the logfile + Start to replay Flight Data - Start to replay the logfile + Start to replay Flight Data - Start to replay the logfile + Start to replay Flight Data ... @@ -129,23 +129,23 @@ - No logfile selected.. + No Flight Data selected.. - Select the logfile to replay + Select the Flight Data to replay - Select the logfile to replay + Select the Flight Data to replay - Select the logfile to replay + Select the Flight Data to replay - Replay Logfile + Replay Flight Data diff --git a/src/ui/QGCStatusBar.cc b/src/ui/QGCStatusBar.cc index 937d6fc0f666f4bc6960e5c8105c6db08489e555..9f771b53d44015e0ccc3747cc729bdfaa32caf48 100644 --- a/src/ui/QGCStatusBar.cc +++ b/src/ui/QGCStatusBar.cc @@ -27,21 +27,15 @@ This file is part of the QGROUNDCONTROL project #include "QGCStatusBar.h" #include "UASManager.h" #include "MainWindow.h" +#include "QGCCore.h" QGCStatusBar::QGCStatusBar(QWidget *parent) : QStatusBar(parent), - toggleLoggingButton(NULL), player(NULL), changed(true), lastLogDirectory(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)) { setObjectName("QGC_STATUSBAR"); - - toggleLoggingButton = new QPushButton(tr("Log to file"), this); - toggleLoggingButton->setCheckable(true); - - addPermanentWidget(toggleLoggingButton); - loadSettings(); } @@ -58,58 +52,6 @@ void QGCStatusBar::setLogPlayer(QGCMAVLinkLogPlayer* player) { this->player = player; addPermanentWidget(player); - connect(toggleLoggingButton, SIGNAL(clicked(bool)), this, SLOT(logging(bool))); -} - -void QGCStatusBar::logging(bool checked) -{ - // Stop logging in any case - MainWindow::instance()->getMAVLink()->enableLogging(false); - - if (!checked && player) - { - player->setLastLogFile(lastLogDirectory); - } - - // If the user is enabling logging - if (checked) - { - // Prompt the user for a filename/location to save to - QString fileName = QFileDialog::getSaveFileName(this, tr("Specify MAVLink log file to save to"), lastLogDirectory, tr("MAVLink Logfile (*.mavlink *.log *.bin);;")); - - // Check that they didn't cancel out - if (fileName.isNull()) - { - toggleLoggingButton->setChecked(false); - return; - } - - // Make sure the file's named properly - if (!fileName.endsWith(".mavlink")) - { - fileName.append(".mavlink"); - } - - // Check that we can save the logfile - QFileInfo file(fileName); - if ((file.exists() && !file.isWritable())) - { - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText(tr("The selected logfile is not writable")); - msgBox.setInformativeText(tr("Please make sure that the file %1 is writable or select a different file").arg(fileName)); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.exec(); - } - // Otherwise we're off and logging - else - { - MainWindow::instance()->getMAVLink()->setLogfileName(fileName); - MainWindow::instance()->getMAVLink()->enableLogging(true); - lastLogDirectory = file.absoluteDir().absolutePath(); //save last log directory - } - } } void QGCStatusBar::loadSettings() @@ -132,5 +74,4 @@ void QGCStatusBar::storeSettings() QGCStatusBar::~QGCStatusBar() { storeSettings(); - if (toggleLoggingButton) toggleLoggingButton->deleteLater(); } diff --git a/src/ui/QGCStatusBar.h b/src/ui/QGCStatusBar.h index 2277db7c9ae7acbd3bdfea2d7f52069a45aa1282..20874240f8d92119dc659a2b648fded6fc68921e 100644 --- a/src/ui/QGCStatusBar.h +++ b/src/ui/QGCStatusBar.h @@ -43,8 +43,6 @@ public: ~QGCStatusBar(); public slots: - /** @brief Start / stop logging */ - void logging(bool checked); /** @brief Set log playing component */ void setLogPlayer(QGCMAVLinkLogPlayer* player); virtual void paintEvent(QPaintEvent * event); @@ -53,7 +51,6 @@ protected: void storeSettings(); void loadSettings(); - QPushButton* toggleLoggingButton; QGCMAVLinkLogPlayer* player; bool changed; QString lastLogDirectory;