/**************************************************************************** * * (c) 2009-2016 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. * ****************************************************************************/ /** * @file * @brief Implementation of class MAVLinkProtocol * @author Lorenz Meier */ #include #include #include #include #include #include #include #include #include #include #include #include "MAVLinkProtocol.h" #include "UASInterface.h" #include "UASInterface.h" #include "UAS.h" #include "LinkManager.h" #include "QGCMAVLink.h" #include "QGC.h" #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include "MultiVehicleManager.h" #include "SettingsManager.h" Q_DECLARE_METATYPE(mavlink_message_t) QGC_LOGGING_CATEGORY(MAVLinkProtocolLog, "MAVLinkProtocolLog") 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. */ MAVLinkProtocol::MAVLinkProtocol(QGCApplication* app, QGCToolbox* toolbox) : QGCTool(app, toolbox) , m_enable_version_check(true) , versionMismatchIgnore(false) , systemId(255) , _current_version(100) , _radio_version_mismatch_count(0) , _logSuspendError(false) , _logSuspendReplay(false) , _vehicleWasArmed(false) , _tempLogFile(QString("%2.%3").arg(_tempLogFileTemplate).arg(_logFileExtension)) , _linkMgr(NULL) , _multiVehicleManager(NULL) { memset(&totalReceiveCounter, 0, sizeof(totalReceiveCounter)); memset(&totalLossCounter, 0, sizeof(totalLossCounter)); memset(&totalErrorCounter, 0, sizeof(totalErrorCounter)); memset(&currReceiveCounter, 0, sizeof(currReceiveCounter)); memset(&currLossCounter, 0, sizeof(currLossCounter)); } MAVLinkProtocol::~MAVLinkProtocol() { storeSettings(); _closeLogFile(); } void MAVLinkProtocol::setVersion(unsigned version) { QList links = _linkMgr->links(); for (int i = 0; i < links.length(); i++) { mavlink_status_t* mavlinkStatus = mavlink_get_channel_status(links[i]->mavlinkChannel()); // Set flags for version if (version < 200) { mavlinkStatus->flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1; } else { mavlinkStatus->flags &= ~MAVLINK_STATUS_FLAG_OUT_MAVLINK1; } } _current_version = version; } void MAVLinkProtocol::setToolbox(QGCToolbox *toolbox) { QGCTool::setToolbox(toolbox); _linkMgr = _toolbox->linkManager(); _multiVehicleManager = _toolbox->multiVehicleManager(); qRegisterMetaType("mavlink_message_t"); loadSettings(); // All the *Counter variables are not initialized here, as they should be initialized // on a per-link basis before those links are used. @see resetMetadataForLink(). // Initialize the list for tracking dropped messages to invalid. for (int i = 0; i < 256; i++) { for (int j = 0; j < 256; j++) { lastIndex[i][j] = -1; } } connect(this, &MAVLinkProtocol::protocolStatusMessage, _app, &QGCApplication::criticalMessageBoxOnMainThread); connect(this, &MAVLinkProtocol::saveTelemetryLog, _app, &QGCApplication::saveTelemetryLogOnMainThread); connect(this, &MAVLinkProtocol::checkTelemetrySavePath, _app, &QGCApplication::checkTelemetrySavePathOnMainThread); connect(_multiVehicleManager, &MultiVehicleManager::vehicleAdded, this, &MAVLinkProtocol::_vehicleCountChanged); connect(_multiVehicleManager, &MultiVehicleManager::vehicleRemoved, this, &MAVLinkProtocol::_vehicleCountChanged); emit versionCheckChanged(m_enable_version_check); } void MAVLinkProtocol::loadSettings() { // Load defaults from settings QSettings settings; settings.beginGroup("QGC_MAVLINK_PROTOCOL"); enableVersionCheck(settings.value("VERSION_CHECK_ENABLED", m_enable_version_check).toBool()); // Only set system id if it was valid int temp = settings.value("GCS_SYSTEM_ID", systemId).toInt(); if (temp > 0 && temp < 256) { systemId = temp; } } void MAVLinkProtocol::storeSettings() { // Store settings QSettings settings; settings.beginGroup("QGC_MAVLINK_PROTOCOL"); settings.setValue("VERSION_CHECK_ENABLED", m_enable_version_check); settings.setValue("GCS_SYSTEM_ID", systemId); // Parameter interface settings } void MAVLinkProtocol::resetMetadataForLink(LinkInterface *link) { int channel = link->mavlinkChannel(); totalReceiveCounter[channel] = 0; totalLossCounter[channel] = 0; totalErrorCounter[channel] = 0; currReceiveCounter[channel] = 0; currLossCounter[channel] = 0; link->setDecodedFirstMavlinkPacket(false); } /** * This method parses all incoming bytes and constructs a MAVLink packet. * It can handle multiple links in parallel, as each link has it's own buffer/ * parsing state machine. * @param link The interface to read from * @see LinkInterface **/ void MAVLinkProtocol::receiveBytes(LinkInterface* link, QByteArray b) { // Since receiveBytes signals cross threads we can end up with signals in the queue // that come through after the link is disconnected. For these we just drop the data // since the link is closed. if (!_linkMgr->containsLink(link)) { return; } // receiveMutex.lock(); mavlink_message_t message; mavlink_status_t status; int mavlinkChannel = link->mavlinkChannel(); static int nonmavlinkCount = 0; static bool checkedUserNonMavlink = false; static bool warnedUserNonMavlink = false; for (int position = 0; position < b.size(); position++) { unsigned int decodeState = mavlink_parse_char(mavlinkChannel, (uint8_t)(b[position]), &message, &status); if (decodeState == 0 && !link->decodedFirstMavlinkPacket()) { nonmavlinkCount++; if (nonmavlinkCount > 1000 && !warnedUserNonMavlink) { // 500 bytes with no mavlink message. Are we connected to a mavlink capable device? if (!checkedUserNonMavlink) { link->requestReset(); checkedUserNonMavlink = true; } else { warnedUserNonMavlink = true; // Disconnect the link since its some other device and // QGC clinging on to it and feeding it data might have unintended // side effects (e.g. if its a modem) qDebug() << "disconnected link" << link->getName() << "as it contained no MAVLink data"; QMetaObject::invokeMethod(_linkMgr, "disconnectLink", Q_ARG( LinkInterface*, link ) ); return; } } } if (decodeState == 1) { if (!link->decodedFirstMavlinkPacket()) { link->setDecodedFirstMavlinkPacket(true); mavlink_status_t* mavlinkStatus = mavlink_get_channel_status(mavlinkChannel); if (!(mavlinkStatus->flags & MAVLINK_STATUS_FLAG_IN_MAVLINK1) && (mavlinkStatus->flags & MAVLINK_STATUS_FLAG_OUT_MAVLINK1)) { qDebug() << "Switching outbound to mavlink 2.0 due to incoming mavlink 2.0 packet:" << mavlinkStatus << mavlinkChannel << mavlinkStatus->flags; mavlinkStatus->flags &= ~MAVLINK_STATUS_FLAG_OUT_MAVLINK1; // Set all links to v2 setVersion(200); } } // Log data if (!_logSuspendError && !_logSuspendReplay && _tempLogFile.isOpen()) { uint8_t buf[MAVLINK_MAX_PACKET_LEN+sizeof(quint64)]; // Write the uint64 time in microseconds in big endian format before the message. // This timestamp is saved in UTC time. We are only saving in ms precision because // getting more than this isn't possible with Qt without a ton of extra code. quint64 time = (quint64)QDateTime::currentMSecsSinceEpoch() * 1000; qToBigEndian(time, buf); // Then write the message to the buffer int len = mavlink_msg_to_send_buffer(buf + sizeof(quint64), &message); // Determine how many bytes were written by adding the timestamp size to the message size len += sizeof(quint64); // Now write this timestamp/message pair to the log. QByteArray b((const char*)buf, len); if(_tempLogFile.write(b) != len) { // If there's an error logging data, raise an alert and stop logging. emit protocolStatusMessage(tr("MAVLink Protocol"), tr("MAVLink Logging failed. Could not write to file %1, logging disabled.").arg(_tempLogFile.fileName())); _stopLogging(); _logSuspendError = true; } // Check for the vehicle arming going by. This is used to trigger log save. if (!_vehicleWasArmed && message.msgid == MAVLINK_MSG_ID_HEARTBEAT) { mavlink_heartbeat_t state; mavlink_msg_heartbeat_decode(&message, &state); if (state.base_mode & MAV_MODE_FLAG_DECODE_POSITION_SAFETY) { _vehicleWasArmed = true; } } } if (message.msgid == MAVLINK_MSG_ID_HEARTBEAT) { // Start loggin on first heartbeat _startLogging(); mavlink_heartbeat_t heartbeat; mavlink_msg_heartbeat_decode(&message, &heartbeat); emit vehicleHeartbeatInfo(link, message.sysid, message.compid, heartbeat.mavlink_version, heartbeat.autopilot, heartbeat.type); } // Detect if we are talking to an old radio not supporting v2 mavlink_status_t* mavlinkStatus = mavlink_get_channel_status(mavlinkChannel); if (message.msgid == MAVLINK_MSG_ID_RADIO_STATUS) { if ((mavlinkStatus->flags & MAVLINK_STATUS_FLAG_IN_MAVLINK1) && !(mavlinkStatus->flags & MAVLINK_STATUS_FLAG_OUT_MAVLINK1)) { _radio_version_mismatch_count++; } } if (_radio_version_mismatch_count == 5) { // Warn the user if the radio continues to send v1 while the link uses v2 emit protocolStatusMessage(tr("MAVLink Protocol"), tr("Detected radio still using MAVLink v1.0 on a link with MAVLink v2.0 enabled. Please upgrade the radio firmware.")); // Ensure the warning can't get stuck _radio_version_mismatch_count++; // Flick link back to v1 qDebug() << "Switching outbound to mavlink 1.0 due to incoming mavlink 1.0 packet:" << mavlinkStatus << mavlinkChannel << mavlinkStatus->flags; mavlinkStatus->flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1; } // Increase receive counter totalReceiveCounter[mavlinkChannel]++; currReceiveCounter[mavlinkChannel]++; // Determine what the next expected sequence number is, accounting for // never having seen a message for this system/component pair. int lastSeq = lastIndex[message.sysid][message.compid]; int expectedSeq = (lastSeq == -1) ? message.seq : (lastSeq + 1); // And if we didn't encounter that sequence number, record the error if (message.seq != expectedSeq) { // Determine how many messages were skipped int lostMessages = message.seq - expectedSeq; // Out of order messages or wraparound can cause this, but we just ignore these conditions for simplicity if (lostMessages < 0) { lostMessages = 0; } // And log how many were lost for all time and just this timestep totalLossCounter[mavlinkChannel] += lostMessages; currLossCounter[mavlinkChannel] += lostMessages; } // And update the last sequence number for this system/component pair lastIndex[message.sysid][message.compid] = expectedSeq; // Update on every 32th packet if ((totalReceiveCounter[mavlinkChannel] & 0x1F) == 0) { // Calculate new loss ratio // Receive loss float receiveLossPercent = (double)currLossCounter[mavlinkChannel]/(double)(currReceiveCounter[mavlinkChannel]+currLossCounter[mavlinkChannel]); receiveLossPercent *= 100.0f; currLossCounter[mavlinkChannel] = 0; currReceiveCounter[mavlinkChannel] = 0; emit receiveLossPercentChanged(message.sysid, receiveLossPercent); emit receiveLossTotalChanged(message.sysid, totalLossCounter[mavlinkChannel]); } // The packet is emitted as a whole, as it is only 255 - 261 bytes short // kind of inefficient, but no issue for a groundstation pc. // It buys as reentrancy for the whole code over all threads emit messageReceived(link, message); } } } /** * @return The name of this protocol **/ QString MAVLinkProtocol::getName() { return QString(tr("MAVLink protocol")); } /** @return System id of this application */ int MAVLinkProtocol::getSystemId() { return systemId; } void MAVLinkProtocol::setSystemId(int id) { systemId = id; } /** @return Component id of this application */ int MAVLinkProtocol::getComponentId() { return 0; } void MAVLinkProtocol::enableVersionCheck(bool enabled) { m_enable_version_check = enabled; emit versionCheckChanged(enabled); } void MAVLinkProtocol::_vehicleCountChanged(void) { int count = _multiVehicleManager->vehicles()->count(); if (count == 0) { // Last vehicle is gone, close out logging _stopLogging(); _radio_version_mismatch_count = 0; } } /// @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) { //-- Are we supposed to write logs? if (qgcApp()->runningUnitTests()) { return; } #ifdef __mobile__ //-- Mobile build don't write to /tmp unless told to do so if (!_app->toolbox()->settingsManager()->appSettings()->telemetrySave()->rawValue().toBool()) { return; } #endif //-- Log is always written to a temp file. If later the user decides they want // it, it's all there for them. if (!_tempLogFile.isOpen()) { if (!_logSuspendReplay) { if (!_tempLogFile.open()) { emit protocolStatusMessage(tr("MAVLink Protocol"), tr("Opening Flight Data file for writing failed. " "Unable to write to %1. Please choose a different file location.").arg(_tempLogFile.fileName())); _closeLogFile(); _logSuspendError = true; return; } qDebug() << "Temp log" << _tempLogFile.fileName(); emit checkTelemetrySavePath(); _logSuspendError = false; } } } void MAVLinkProtocol::_stopLogging(void) { if (_tempLogFile.isOpen()) { if (_closeLogFile()) { if ((_vehicleWasArmed || _app->toolbox()->settingsManager()->appSettings()->telemetrySaveNotArmed()->rawValue().toBool()) && _app->toolbox()->settingsManager()->appSettings()->telemetrySave()->rawValue().toBool()) { emit saveTelemetryLog(_tempLogFile.fileName()); } else { QFile::remove(_tempLogFile.fileName()); } } } _vehicleWasArmed = false; } /// @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::checkForLostLogFiles(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; } emit saveTelemetryLog(fileInfo.filePath()); } } void MAVLinkProtocol::suspendLogForReplay(bool suspend) { _logSuspendReplay = suspend; } void MAVLinkProtocol::deleteTempLogFiles(void) { QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); QString filter(QString("*.%1").arg(_logFileExtension)); QFileInfoList fileInfoList = tempDir.entryInfoList(QStringList(filter), QDir::Files); foreach(const QFileInfo fileInfo, fileInfoList) { QFile::remove(fileInfo.filePath()); } }